From b0225fab5b8c81695ebdc641a62ad683cacb61d6 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Tue, 15 Jun 2021 15:28:44 -0700 Subject: [PATCH 01/30] Swap query variables with their values --- .eslintrc.json | 21 +++++++++++++++ src/CacheClassServer.js | 5 ++-- src/destructure.js | 51 +++++++++++++++++++++++++++++++----- src/obsidian.ts | 2 +- src/server/App.tsx | 21 +++++++++++++++ src/server/MainContainer.tsx | 27 +++++++++++++++++++ src/server/client.tsx | 5 ++++ tsconfig.json | 13 ++++++--- 8 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 .eslintrc.json create mode 100644 src/server/App.tsx create mode 100644 src/server/MainContainer.tsx create mode 100644 src/server/client.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ea50e50 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": ["airbnb", "prettier"], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "plugins": ["react", "prettier"], + "rules": { + "prettier/prettier": "error", + "no-console": "off", + "useTabs": "off", + "react/jsx-filename-extension": "off" + } +} diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 94133a3..2ec0380 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -32,11 +32,11 @@ 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 = {}; @@ -45,6 +45,7 @@ export class Cache { // 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'); + console.log('CACHE: \n\n', this.storage); // match in ROOT_QUERY if (rootQuery[queryHash]) { // get the hashs to populate from the existent query in the cache diff --git a/src/destructure.js b/src/destructure.js index 3e961b4..015ff7b 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -12,7 +12,7 @@ * */ // this function will destructure a query/mutation operation string into a query/mutation operation object -export function destructureQueries(queryOperationStr) { +export function destructureQueries(queryOperationStr, queryOperationVars) { queryOperationStr = queryOperationStr.replace(/,/gm, ''); // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { @@ -31,7 +31,13 @@ export function destructureQueries(queryOperationStr) { ? 'mutations' : 'queries'; // create a queries object from array of query strings - const queriesObj = createQueriesObj(arrayOfQueryStrings, typePropName); + const queriesObj = createQueriesObj( + arrayOfQueryStrings, + queryOperationVars, + typePropName + ); + + console.log('QUERY OBJECT: =================\n', queriesObj); return queriesObj; } @@ -72,14 +78,14 @@ export function findQueryStrings(queryStrings) { } // helper function to create a queries object from an array of query strings -export function createQueriesObj(arrayOfQueryStrings, typePropName) { +export function createQueriesObj(arrayOfQueryStrings, queryVars, typePropName) { // define a new empty result object const queriesObj = {}; queriesObj[typePropName] = []; // for each query string arrayOfQueryStrings.forEach((queryStr) => { // split the query string into multiple parts - const queryObj = splitUpQueryStr(queryStr); + const queryObj = splitUpQueryStr(queryStr, queryVars); // recursively convert the fields string to a fields object and update the fields property queryObj.fields = findQueryFields(queryObj.fields); // push the finished query object into the queries/mutations array on the result object @@ -89,7 +95,7 @@ export function createQueriesObj(arrayOfQueryStrings, typePropName) { return queriesObj; } // helper function that returns an object with a query string split into multiple parts -export function splitUpQueryStr(queryStr) { +export function splitUpQueryStr(queryStr, queryVars) { // creates new queryObj const queryObj = {}; let parensPairs = 0; @@ -139,14 +145,45 @@ export function splitUpQueryStr(queryStr) { argsString = argsString.replace(/\s/g, ''); // handles edge case where ther are no arguments inside the argument parens pair. if (argsString === '()') argsString = ''; + // TBD: call replaceQueryVariables() + if (queryVars) { + argsString = replaceQueryVariables(argsString, queryVars); + } + queryObj.arguments = argsString; queryObj.fields = queryStr.substring(i + 1).trim(); - return queryObj; } } } +// helper function to manipulate query args string by replacing variables +export function replaceQueryVariables(queryArgs, variables) { + let varStartIndex; + let varEndIndex; + + for (let i = 0; i < queryArgs.length; i += 1) { + const char = queryArgs[i]; + + if (char === '$') varStartIndex = i; + if (char === ',' || char === ')') varEndIndex = i; + + // !!!!!! "variables" not necessary here !!!!!!! + if (varStartIndex && varEndIndex && variables) { + const varName = queryArgs.slice(varStartIndex + 1, varEndIndex); + const varValue = variables[varName]; + + if (varValue) { + queryArgs = queryArgs.replace(varName, varValue).replace('$', ''); + } + + varStartIndex = undefined; + varEndIndex = undefined; + } + } + return queryArgs; +} + // helper function to recursively convert the fields string to a fields object export function findQueryFields(fieldsStr) { const fieldsObj = {}; @@ -268,4 +305,4 @@ export function destructureQueriesWithFragments(queryOperationStr) { return queryCopy; } -export default destructureQueries; \ No newline at end of file +export default destructureQueries; diff --git a/src/obsidian.ts b/src/obsidian.ts index 9ee6a83..05f8067 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -82,7 +82,7 @@ export async function ObsidianRouter({ if (useCache) { // Send query off to be destructured and found in Redis if possible // - const obsidianReturn = await cache.read(body.query); + const obsidianReturn = await cache.read(body.query, body.variables); console.log('retrieved from cache', obsidianReturn); if (obsidianReturn) { response.status = 200; diff --git a/src/server/App.tsx b/src/server/App.tsx new file mode 100644 index 0000000..92c7cea --- /dev/null +++ b/src/server/App.tsx @@ -0,0 +1,21 @@ +import React from 'https://dev.jspm.io/react'; +import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; +import MainContainer from './MainContainer.tsx'; + +declare global { + namespace JSX { + interface IntrinsicElements { + [elemName: string]: any; + } + } +} + +const App = () => { + return ( + + + + ); +}; + +export default App; diff --git a/src/server/MainContainer.tsx b/src/server/MainContainer.tsx new file mode 100644 index 0000000..280f229 --- /dev/null +++ b/src/server/MainContainer.tsx @@ -0,0 +1,27 @@ +import React from 'https://dev.jspm.io/react'; +import { useObsidian } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const MainContainer = () => { + const { query } = useObsidian(); + const [movie, setMovie] = (React as any).useState({}); + + return ( +
+

Obsidian Film Showcase

+

Check out our favorite movie by clicking the button below

+ +

Title: {movie.title}

+

Release Year: {movie.releaseYear}

+
+ ); +}; + +export default MainContainer; diff --git a/src/server/client.tsx b/src/server/client.tsx new file mode 100644 index 0000000..a200b4b --- /dev/null +++ b/src/server/client.tsx @@ -0,0 +1,5 @@ +import React from 'https://dev.jspm.io/react'; +import ReactDom from 'https://dev.jspm.io/react-dom'; +import App from './App.tsx'; + +(ReactDom as any).hydrate(, document.getElementById('root')); diff --git a/tsconfig.json b/tsconfig.json index c6ae093..f995a9b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,18 @@ { + "compileOnSave": true, "compilerOptions": { "allowJs": true, "target": "ES2017", "jsx": "react", "noImplicitAny": false, - "module": "esnext", + "module": "CommonJS", "strict": true, + "lib": ["dom"] }, - "include":["../src"] + "include": ["./**/*"], + "exclude": [ + "./plugins/**/*", + "./typings/**/*", + "./built/**/*" // This is what fixed it! + ] } - - From f62fe7642dd354573e1608cf8e6f451247db12fb Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Tue, 15 Jun 2021 15:37:00 -0700 Subject: [PATCH 02/30] Store queries with a single variable in cache --- src/CacheClassServer.js | 4 ++-- src/obsidian.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 2ec0380..4fbfd12 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -67,8 +67,8 @@ export class Cache { return { data: responseObject }; } - async write(queryStr, respObj, deleteFlag) { - const queryObj = destructureQueries(queryStr); + async write(queryStr, queryVars, respObj, deleteFlag) { + const queryObj = destructureQueries(queryStr, queryVars); const resFromNormalize = normalizeResult(queryObj, respObj, deleteFlag); // update the original cache with same reference for (const hash in resFromNormalize) { diff --git a/src/obsidian.ts b/src/obsidian.ts index 05f8067..0f6e782 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -113,7 +113,7 @@ export async function ObsidianRouter({ // Normalize response and store in cache // if (useCache && toNormalize && !result.errors) - cache.write(body.query, result, false); + cache.write(body.query, body.variables, result, false); var t1 = performance.now(); console.log( 'Obsidian received new data and took ' + (t1 - t0) + ' milliseconds.' From 8e2582e60c8aa339cffaae79fb9a92ea95475a8f Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Tue, 15 Jun 2021 16:56:06 -0700 Subject: [PATCH 03/30] Store queries with multiple variables in cache --- src/destructure.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/destructure.js b/src/destructure.js index 015ff7b..7223dd1 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -13,7 +13,8 @@ */ // this function will destructure a query/mutation operation string into a query/mutation operation object export function destructureQueries(queryOperationStr, queryOperationVars) { - queryOperationStr = queryOperationStr.replace(/,/gm, ''); + // console.log('RAW QUERY STRING: ', queryOperationStr); + // queryOperationStr = queryOperationStr.replace(/,/gm, ''); // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { // reassigns query string to replace fragment references with fragment fields @@ -157,11 +158,29 @@ export function splitUpQueryStr(queryStr, queryVars) { } } +/* +query Test($movieId: ID, $title: String) { + getMovie(id: $movieId, title: $title ) { + id + title + releaseYear + } +} +Query Arg String: (id:$movieIdtitle:$title) +*/ + // helper function to manipulate query args string by replacing variables export function replaceQueryVariables(queryArgs, variables) { + console.log('Variables object: ', variables); let varStartIndex; let varEndIndex; + console.log('Query Arg String: ', queryArgs); + + // Query Arg String: (id:$movieId,title:$title) + // After replacing: (id:2,title:$title) + // output: (id:2, title:"Movie-2") + for (let i = 0; i < queryArgs.length; i += 1) { const char = queryArgs[i]; @@ -171,16 +190,23 @@ export function replaceQueryVariables(queryArgs, variables) { // !!!!!! "variables" not necessary here !!!!!!! if (varStartIndex && varEndIndex && variables) { const varName = queryArgs.slice(varStartIndex + 1, varEndIndex); + // (id: $movieId, title: $title ) const varValue = variables[varName]; if (varValue) { - queryArgs = queryArgs.replace(varName, varValue).replace('$', ''); + queryArgs = queryArgs.replace('$' + varName, varValue); + i -= varName.length - varValue.length; + // (id:$movieId,title:$title) i = 11 + // (id:2,title:$title) i = 4 + // varName - varValue } varStartIndex = undefined; varEndIndex = undefined; } } + // queryArgs = queryArgs.replace(/[$]/ig, ''); + console.log('After replacing: ', queryArgs); return queryArgs; } From b423920ccd4e5afe75762ab2418973f362fe1f9a Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Tue, 15 Jun 2021 17:03:11 -0700 Subject: [PATCH 04/30] Add comments to destructur.js Co-authored-by: Justin McKay justinmckay99@gmail.com Co-authored-by: Raymond Ahn ray.ahn@gmail.com --- src/destructure.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/destructure.js b/src/destructure.js index 7223dd1..c1234cd 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -171,12 +171,9 @@ Query Arg String: (id:$movieIdtitle:$title) // helper function to manipulate query args string by replacing variables export function replaceQueryVariables(queryArgs, variables) { - console.log('Variables object: ', variables); let varStartIndex; let varEndIndex; - console.log('Query Arg String: ', queryArgs); - // Query Arg String: (id:$movieId,title:$title) // After replacing: (id:2,title:$title) // output: (id:2, title:"Movie-2") @@ -205,8 +202,6 @@ export function replaceQueryVariables(queryArgs, variables) { varEndIndex = undefined; } } - // queryArgs = queryArgs.replace(/[$]/ig, ''); - console.log('After replacing: ', queryArgs); return queryArgs; } From a2e9900169ea5e5a50a6092fb3ce4342e5716c08 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:24:31 -0700 Subject: [PATCH 05/30] Replace variables in directives with their values --- src/destructure.js | 67 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/src/destructure.js b/src/destructure.js index c1234cd..eafe76e 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -9,18 +9,27 @@ * 7. We won't handle variables for now, but we may very well find we need to * 8. We will handle only the meta field "__typename" for now * 9. What edge cases as far as field/query names do we have to worry about: special characters, apostrophes, etc??? + * 10. Directives-implementation don't handle fragment inclusion * */ + // this function will destructure a query/mutation operation string into a query/mutation operation object export function destructureQueries(queryOperationStr, queryOperationVars) { - // console.log('RAW QUERY STRING: ', queryOperationStr); - // queryOperationStr = queryOperationStr.replace(/,/gm, ''); // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { // reassigns query string to replace fragment references with fragment fields queryOperationStr = destructureQueriesWithFragments(queryOperationStr); } + // check if query has directives + if (queryOperationStr.indexOf('@') !== -1) { + // reassigns query string to handle directives + queryOperationStr = destructureQueriesWithDirectives( + queryOperationStr, + queryOperationVars + ); + } + // ignore operation name by finding the beginning of the query strings const startIndex = queryOperationStr.indexOf('{'); const queryStrings = queryOperationStr.substring(startIndex).trim(); @@ -34,8 +43,8 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { // create a queries object from array of query strings const queriesObj = createQueriesObj( arrayOfQueryStrings, - queryOperationVars, - typePropName + typePropName, + queryOperationVars ); console.log('QUERY OBJECT: =================\n', queriesObj); @@ -79,7 +88,7 @@ export function findQueryStrings(queryStrings) { } // helper function to create a queries object from an array of query strings -export function createQueriesObj(arrayOfQueryStrings, queryVars, typePropName) { +export function createQueriesObj(arrayOfQueryStrings, typePropName, queryVars) { // define a new empty result object const queriesObj = {}; queriesObj[typePropName] = []; @@ -159,14 +168,14 @@ export function splitUpQueryStr(queryStr, queryVars) { } /* -query Test($movieId: ID, $title: String) { - getMovie(id: $movieId, title: $title ) { - id - title - releaseYear +query Hero($episode: Episode, $withFriends: Boolean!) { + hero(episode: $episode) { + name + friends @include(if: $withFriends) { + name } + } } -Query Arg String: (id:$movieIdtitle:$title) */ // helper function to manipulate query args string by replacing variables @@ -326,4 +335,40 @@ export function destructureQueriesWithFragments(queryOperationStr) { return queryCopy; } +export function destructureQueriesWithDirectives(queryStr, queryVars) { + /* + query Hero($episode: Episode, $withFriends: Boolean!) { + hero(episode: $episode) { + name + friends @include(if: true) { + name + } + } + } +*/ + const startIndex = queryStr.indexOf('{'); + + let argStartIndex; + let argEndIndex; + + for (let i = startIndex; i < queryStr.length; i += 1) { + const char = queryStr[i]; + + if (char === '(') argStartIndex = i; + if (char === ')') argEndIndex = i; + + if (argStartIndex && argEndIndex) { + const oldQueryArgs = queryStr.slice(argStartIndex, argEndIndex + 1); + const newQueryArgs = replaceQueryVariables(oldQueryArgs, queryVars); + + queryStr = queryStr.replace(oldQueryArgs, newQueryArgs); + + argStartIndex = undefined; + argEndIndex = undefined; + } + } + + return queryStr; +} + export default destructureQueries; From e93022571bd371d188e91d43eaa4306ca76de85b Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Thu, 17 Jun 2021 12:27:38 -0700 Subject: [PATCH 06/30] Destructure queries with directives --- src/destructure.js | 105 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/src/destructure.js b/src/destructure.js index eafe76e..9976f10 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -15,6 +15,13 @@ // this function will destructure a query/mutation operation string into a query/mutation operation object export function destructureQueries(queryOperationStr, queryOperationVars) { + queryOperationStr = queryOperationStr.replace(/\s+/g, ' ').trim(); + + // console.log('QUERY STR WITH REGEX: ', queryOperationStr); + + // query Hero($movieId: ID, $withRel: Boolean!) { getMovie(id: $movieId) { id + // releaseYear @include(if: $withRel) title } } + // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { // reassigns query string to replace fragment references with fragment fields @@ -199,7 +206,7 @@ export function replaceQueryVariables(queryArgs, variables) { // (id: $movieId, title: $title ) const varValue = variables[varName]; - if (varValue) { + if (varValue !== undefined) { queryArgs = queryArgs.replace('$' + varName, varValue); i -= varName.length - varValue.length; // (id:$movieId,title:$title) i = 11 @@ -346,7 +353,7 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } */ - const startIndex = queryStr.indexOf('{'); + let startIndex = queryStr.indexOf('{'); let argStartIndex; let argEndIndex; @@ -368,7 +375,101 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } + console.log('queryStr after replacing: ', queryStr); + + startIndex = queryStr.indexOf('@'); + let includeQueryField = false; + let startDeleteIndex; + let endDeleteIndex; + + // check in between @ and closing parens + for (let i = startIndex; i < queryStr.length; i += 1) { + const char = queryStr[i]; + + if (char === '@') { + startDeleteIndex = i; + } + + if (char === ')') { + endDeleteIndex = i; + } + + // if directive is true + if (startDeleteIndex && char === ':') { + if (queryStr.slice(i, i + 6).indexOf('true') !== -1) { + includeQueryField = true; + } + } + + if (startDeleteIndex && endDeleteIndex) { + const directive = queryStr.slice(startDeleteIndex, endDeleteIndex + 2); + + queryStr = queryStr.replace(directive, ''); + i -= directive.length; + + if (!includeQueryField) { + let j = i + 3; + + // Delete the body of field + if (queryStr.slice(i, j).indexOf('{') !== -1) { + let numOpeningBrace = 0; + let numClosingBrace = 0; + + while (j >= 0) { + if (queryStr[j--] === '{') numOpeningBrace++; + } + + // start at the end of the queryStr and count # of closing braces.. + let k = queryStr.length - 1; + + while (numClosingBrace !== numOpeningBrace) { + if (queryStr[k--] === '}') numClosingBrace++; + } + + const openingBracketIndex = i; + const closingBracketIndex = k + 1; + + queryStr = queryStr.replace( + queryStr.slice(openingBracketIndex, closingBracketIndex + 1), + '' + ); + } + + // Delete the field with the directive attached to it + let startFieldNameIndex = i - 1; + + while (queryStr[startFieldNameIndex] !== ' ') { + startFieldNameIndex--; + } + + queryStr = queryStr.replace(queryStr.slice(startFieldNameIndex, i), ''); + } + + // Otherwise, remove the "directive" string, the following body {} if any, + // and the preceding field name + + startDeleteIndex = undefined; + endDeleteIndex = undefined; + } + } + + console.log('AFTER REPLACEMENT QUERYSTR: ', queryStr); + return queryStr; } export default destructureQueries; + +// query Hero($movieId: ID, $withRel: Boolean!) + +// { +// getMovie(id: 1) { +// id +// releaseYear @include(if: true) { +// name { +// student +// } +// } +// title +// } +// } From 51644d6e8dc8bc3c1bf50fd8c37edc9ba6709158 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Sat, 19 Jun 2021 15:16:46 -0700 Subject: [PATCH 07/30] Implemented DoS security module enabling developers to set a maximum query nesting depth value, above which, queries will be rejected. --- .vscode/settings.json | 5 ++++ src/DoSSecurity.ts | 55 +++++++++++++++++++++++++++++++++++++++++++ src/destructure.js | 22 ++++++++--------- src/obsidian.ts | 22 ++++++++++++----- 4 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/DoSSecurity.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1535e13 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true +} \ No newline at end of file diff --git a/src/DoSSecurity.ts b/src/DoSSecurity.ts new file mode 100644 index 0000000..18fc450 --- /dev/null +++ b/src/DoSSecurity.ts @@ -0,0 +1,55 @@ +import destructureQueries from './destructure.js'; + +interface queryObj { + queries?: Array, + mutations?: Array, +} +/** + * Tests whether a queryString (string representation of query) exceeds the maximum nested depth levels (queryDephtLimit) 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 queryDepthExceeded(queryString: string, queryDepthLimit: number = 0): void { + const queryObj = destructureQueries(queryString) as queryObj; + // Make a subfunction called 'queryDepthCheck', params queryObj, queryDepthLimit, depth and returns boolean + /** + *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 => { + // check if queryObj has query or mutation root type, if so, call query depth check on each of its elements + if (Object.prototype.hasOwnProperty.call(qryObj, 'queries')) { + qryObj.queries?.forEach((element: object) => { + queryDepthCheck(element, queryDepthLimit); + }); + } + if (Object.prototype.hasOwnProperty.call(qryObj, 'mutations')) { + qryObj.mutations?.forEach((element: object) => { + queryDepthCheck(element, queryDepthLimit); + }); + } + + // base case 1: check to see if depth exceeds limit, if so, return error (true => depth has been exceeded) + if (depth > qryDepthLim) return true; + // Iterate through values of queryObj, and check if it is an object, + for (let value = 0; value < Object.values(qryObj).length; value++) { + // if so return invoked QDC with depth+1 + 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; + }; + // Invoke QDC and if returns true return error, else return nothing + if (queryDepthCheck(queryObj, queryDepthLimit)) { + throw new Error( + 'Security Error: Query depth exceeded maximum query depth limit' + ); + } +} diff --git a/src/destructure.js b/src/destructure.js index 3e961b4..7a00c70 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -12,7 +12,7 @@ * */ // this function will destructure a query/mutation operation string into a query/mutation operation object -export function destructureQueries(queryOperationStr) { +export default function destructureQueries(queryOperationStr) { queryOperationStr = queryOperationStr.replace(/,/gm, ''); // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { @@ -209,8 +209,8 @@ export function destructureQueriesWithFragments(queryOperationStr) { const fragments = []; // helper function to separate fragment from query/mutation const separateFragments = (queryCopy) => { - let startFragIndex = queryCopy.indexOf('fragment'); - let startFragCurly = queryCopy.indexOf('{', startFragIndex); + const startFragIndex = queryCopy.indexOf('fragment'); + const startFragCurly = queryCopy.indexOf('{', startFragIndex); let endFragCurly; const stack = ['{']; const curlsAndParens = { @@ -218,12 +218,12 @@ export function destructureQueriesWithFragments(queryOperationStr) { ')': '(', }; for (let i = startFragCurly + 1; i < queryCopy.length; i++) { - let char = queryCopy[i]; + const char = queryCopy[i]; if (char === '{' || char === '(') { stack.push(char); } if (char === '}' || char === ')') { - let topOfStack = stack[stack.length - 1]; + const topOfStack = stack[stack.length - 1]; if (topOfStack === curlsAndParens[char]) stack.pop(); } if (!stack[0]) { @@ -232,10 +232,10 @@ export function destructureQueriesWithFragments(queryOperationStr) { } } - let fragment = queryCopy.slice(startFragIndex, endFragCurly + 1); + const fragment = queryCopy.slice(startFragIndex, endFragCurly + 1); fragments.push(fragment); - let newStr = queryCopy.replace(fragment, ''); + const newStr = queryCopy.replace(fragment, ''); return newStr; }; @@ -252,9 +252,9 @@ export function destructureQueriesWithFragments(queryOperationStr) { //! TODO: OPTIMIZE, SHOULD NOT NEED TO ITERATE THROUGH WHOLE QUERY STRING TO FIND THE ONE WORD NAME OF THE FRAGMENT. MAYBE WHILE STRING INDEX< INDEX OF '{' ? // store each fragment name with its corresponding fields in fragmentObj fragments.forEach((fragment) => { - let index = fragment.indexOf('{'); - let words = fragment.split(' '); - let fragmentFields = fragment.slice(index + 1, fragment.length - 1); + const index = fragment.indexOf('{'); + const words = fragment.split(' '); + const fragmentFields = fragment.slice(index + 1, fragment.length - 1); fragmentObj[words[1]] = fragmentFields; }); @@ -267,5 +267,3 @@ export function destructureQueriesWithFragments(queryOperationStr) { return queryCopy; } - -export default destructureQueries; \ No newline at end of file diff --git a/src/obsidian.ts b/src/obsidian.ts index 9ee6a83..47e4d9a 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -3,6 +3,7 @@ import { renderPlaygroundPage } from 'https://deno.land/x/oak_graphql@0.6.2/grap import { makeExecutableSchema } from 'https://deno.land/x/oak_graphql@0.6.2/graphql-tools/schema/makeExecutableSchema.ts'; import LFUCache from './lfuBrowserCache.js'; import { Cache } from './CacheClassServer.js'; +import queryDepthLimiter from './DoSSecurity.ts'; interface Constructable { new (...args: any): T & OakRouter; @@ -25,6 +26,7 @@ export interface ObsidianRouterOptions { redisPort?: number; policy?: string; maxmemory?: string; + maxQueryDepth?: number; } export interface ResolversProps { @@ -47,6 +49,7 @@ export async function ObsidianRouter({ redisPort = 6379, policy, maxmemory, + maxQueryDepth = 0, }: ObsidianRouterOptions): Promise { redisPortExport = redisPort; const router = new Router(); @@ -65,7 +68,7 @@ export async function ObsidianRouter({ // set redis configurations if (policy || maxmemory) { - console.log('inside if'); + // console.log('inside if'); cache.configSet('maxmemory-policy', policy); cache.configSet('maxmemory', maxmemory); } @@ -77,6 +80,11 @@ export async function ObsidianRouter({ try { const contextResult = context ? await context(ctx) : undefined; const body = await request.body().value; + + // If a securty limit is set for maxQueryDepth, invoke queryDepthLimiter + // which throws error if query depth exceeds maximum + if (maxQueryDepth) queryDepthLimiter(body.query, maxQueryDepth); + // Variable to block the normalization of mutations // let toNormalize = true; @@ -90,8 +98,8 @@ export async function ObsidianRouter({ var t1 = performance.now(); console.log( 'Obsidian retrieved data from cache and took ' + - (t1 - t0) + - ' milliseconds.' + (t1 - t0) + + ' milliseconds.', ); return; } @@ -104,7 +112,7 @@ export async function ObsidianRouter({ resolvers, contextResult, body.variables || undefined, - body.operationName || undefined + body.operationName || undefined, ); // Send database response to client // @@ -112,11 +120,12 @@ export async function ObsidianRouter({ response.body = result; // Normalize response and store in cache // - if (useCache && toNormalize && !result.errors) + if (useCache && toNormalize && !result.errors) { cache.write(body.query, result, false); + } var t1 = performance.now(); console.log( - 'Obsidian received new data and took ' + (t1 - t0) + ' milliseconds.' + 'Obsidian received new data and took ' + (t1 - t0) + ' milliseconds.', ); return; } catch (error) { @@ -129,6 +138,7 @@ export async function ObsidianRouter({ }, ], }; + console.error('Error: ', error.message); return; } } From 4dbc2dd835d9e13d5042e8d7e9dcc7948662db07 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Wed, 23 Jun 2021 16:32:43 -0700 Subject: [PATCH 08/30] Fix cache read() when query has nested types --- src/CacheClassServer.js | 91 +++++++++++++++++++++++++++++++++++++++-- src/destructure.js | 9 ++-- src/lfuBrowserCache.js | 1 + src/obsidian.ts | 2 +- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 4fbfd12..01a48e8 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -37,19 +37,34 @@ export class Cache { throw TypeError('input should be a string'); // destructure the query string into an object const queries = destructureQueries(queryStr, queryVars).queries; + // console.log('QUERIES from .read() after destructure: ', queries); // breaks out of function if queryStr is a mutation + // DOES IT EVER RETURN UNDEFINED THO???? <-- lol 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); + // RETURNS {} INITIALLY, BEFORE CACHE WRITE OCCURS const rootQuery = await this.cacheRead('ROOT_QUERY'); - console.log('CACHE: \n\n', this.storage); + // match in ROOT_QUERY + console.log('ROOT QUERY: ', rootQuery); + console.log('\n\n'); + console.log('QUERYHASH: ', queryHash); + console.log('\n\n'); + console.log('rootQuery[queryHash]: ', rootQuery[queryHash]); + console.log('\n\n'); + if (rootQuery[queryHash]) { // get the hashs to populate from the existent query in the cache + /* + - WHY IS ROOTQUERY[QUERYHASH] === UNDEFINED~2 ? ? ? ? + - maybe rename to arrayOfHashes + */ const arrayHashes = rootQuery[queryHash]; + console.log('arrayHashes: ', arrayHashes); // Determines responseObject property labels - use alias if applicable, otherwise use name const respObjProp = queries[query].alias ?? queries[query].name; // invoke populateAllHashes and add data objects to the response object for each input query @@ -57,6 +72,8 @@ export class Cache { arrayHashes, queries[query].fields ); + + console.log('OUTPUT OF popAllHashes: ', responseObject[respObjProp]); if (!responseObject[respObjProp]) return undefined; // no match with ROOT_QUERY return null or ... @@ -88,6 +105,7 @@ export class Cache { async cacheRead(hash) { // 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 @@ -102,7 +120,12 @@ export class Cache { } } let hashedQuery = await redis.get(hash); + console.log( + `hashedQuery from cacheRead for hash: ${hash} ==== `, + hashedQuery + ); // if cacheRead is a miss + // MAYBE IF IT IS AN EMPTY OBJECT? NOT UNDEFINED???? if (hashedQuery === undefined) return undefined; return JSON.parse(hashedQuery); } @@ -157,13 +180,23 @@ export class Cache { // specialized helper methods async populateAllHashes(allHashesFromQuery, fields) { + console.log('popAllHashes has been invoked!!!!!!!'); + console.log('====== allHashesFromQuery before any manipulation: '); + console.log(allHashesFromQuery); // include the hashname for each hash if (!allHashesFromQuery.length) return []; const hyphenIdx = allHashesFromQuery[0].indexOf('~'); + // SHOULD TYPENAME EVER BE UNDEFINED??? WHERE IS THIS TYPENAME + // BEING SET TO UNDEFINED from ??? const typeName = allHashesFromQuery[0].slice(0, hyphenIdx); + + // fields: { id: "scalar", title: "scalar", releaseYear: { year: "scalar" } } + return allHashesFromQuery.reduce(async (acc, hash) => { + // readVal: { id: "1", title: "Movie-1", releaseYear: 2001 } // for each hash from the input query, build the response object const readVal = await this.cacheRead(hash); + console.log('readVal: ', readVal); // return undefine if hash has been garbage collected if (readVal === undefined) return undefined; if (readVal === 'DELETED') return acc; @@ -172,16 +205,34 @@ export class Cache { if (readVal[field] === 'DELETED') continue; // for each field in the fields input query, add the corresponding value from the cache if the field is not another array of hashs if (readVal[field] === undefined && field !== '__typename') { + console.log('1'); return undefined; } else if (typeof fields[field] !== 'object') { + console.log('2'); // add the typename for the type if (field === '__typename') { + console.log('3'); dataObj[field] = typeName; - } else dataObj[field] = readVal[field]; + } else { + console.log('3.5', readVal[field]); + // console.log() + dataObj[field] = readVal[field]; + } } else { + console.log('4'); // case where the field from the input query is an array of hashes, recursively invoke populateAllHashes + // *PROBLEM HERE* - What does populateAllHashes here do? - What is inside releaseYear? + // exepected output of calling populateAllHashes: releaseYear : { year: "scalar" } + + // fields: + // { id: "scalar", title: "scalar", releaseYear: { year: "scalar" } } + // readVal: + // { id: "1", title: "Movie-1", releaseYear: 2001 } + + console.log('readVal[field]: ', readVal[field]); + dataObj[field] = await this.populateAllHashes( - readVal[field], + [readVal[field]], // <------ wrong type // changed to array type fields[field] ); if (dataObj[field] === undefined) return undefined; @@ -198,3 +249,37 @@ export class Cache { }, []); } } + +/* + query Test { + getMovie(id: 2) { + title + releaseYear { + year + } + } + } +*/ + +/* +{ + "data": null, + "errors": [ + { + "message": "allHashesFromQuery.reduce is not a function" <--- comes from populateAllHashes method in cacheClassServer + } + ] +} +*/ + +// TO DO: +// - Caching doesn't work with Directives if variable is false only +// - Console log the query objects +// - (query body from the cache needs array bracket to indicate it's retrieved from cache) +// - Look into cache tests: +// - do any of the test queries they use have nested "bodies" +// - YES, but their tests passed but we know that the cache wasnt +// working for "nested body" queries + +// Directives -- skip +// Write our own tests on Rhum diff --git a/src/destructure.js b/src/destructure.js index 9976f10..40d8cba 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -15,6 +15,7 @@ // this function will destructure a query/mutation operation string into a query/mutation operation object export function destructureQueries(queryOperationStr, queryOperationVars) { + console.log('================ NEW ====================='); queryOperationStr = queryOperationStr.replace(/\s+/g, ' ').trim(); // console.log('QUERY STR WITH REGEX: ', queryOperationStr); @@ -54,7 +55,7 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { queryOperationVars ); - console.log('QUERY OBJECT: =================\n', queriesObj); + // console.log('QUERY OBJECT: =================\n', queriesObj); return queriesObj; } @@ -375,8 +376,6 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } - console.log('queryStr after replacing: ', queryStr); - startIndex = queryStr.indexOf('@'); let includeQueryField = false; let startDeleteIndex; @@ -426,7 +425,7 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { if (queryStr[k--] === '}') numClosingBrace++; } - const openingBracketIndex = i; + const openingBracketIndex = i - 1; const closingBracketIndex = k + 1; queryStr = queryStr.replace( @@ -453,8 +452,6 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } - console.log('AFTER REPLACEMENT QUERYSTR: ', queryStr); - return queryStr; } diff --git a/src/lfuBrowserCache.js b/src/lfuBrowserCache.js index dabf5f1..3ff7c90 100644 --- a/src/lfuBrowserCache.js +++ b/src/lfuBrowserCache.js @@ -136,6 +136,7 @@ LFUCache.prototype.read = async function (queryStr) { arrayHashes, queries[query].fields ); + console.log('reaches lfucache func here'); if (!responseObject[respObjProp]) return undefined; // no match with ROOT_QUERY return null or ... diff --git a/src/obsidian.ts b/src/obsidian.ts index 0f6e782..b90ac73 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -83,7 +83,7 @@ export async function ObsidianRouter({ if (useCache) { // Send query off to be destructured and found in Redis if possible // const obsidianReturn = await cache.read(body.query, body.variables); - console.log('retrieved from cache', obsidianReturn); + console.log('Retrieved from cache: \n\t', obsidianReturn); if (obsidianReturn) { response.status = 200; response.body = obsidianReturn; From 03b86caeb812843982744504a60d0e9603abec35 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 11:11:57 -0700 Subject: [PATCH 09/30] Refactored DoS Security module and added DoS Securuty tests --- src/DoSSecurity.ts | 58 ++++++++++--------- .../rhum_test_files/DoSSecurity_test.ts | 40 +++++++++++++ .../test_variables/DoSSecurity_variables.ts | 30 ++++++++++ 3 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 test_files/rhum_test_files/DoSSecurity_test.ts create mode 100644 test_files/test_variables/DoSSecurity_variables.ts diff --git a/src/DoSSecurity.ts b/src/DoSSecurity.ts index 18fc450..12deaa3 100644 --- a/src/DoSSecurity.ts +++ b/src/DoSSecurity.ts @@ -1,18 +1,19 @@ import destructureQueries from './destructure.js'; +// Interface representing shape of query object after destructuring interface queryObj { queries?: Array, mutations?: Array, } + /** * Tests whether a queryString (string representation of query) exceeds the maximum nested depth levels (queryDephtLimit) 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 queryDepthExceeded(queryString: string, queryDepthLimit: number = 0): void { +export default function queryDepthLimiter(queryString: string, queryDepthLimit: number = 0): void { const queryObj = destructureQueries(queryString) as queryObj; - // Make a subfunction called 'queryDepthCheck', params queryObj, queryDepthLimit, depth and returns boolean /** *Function that tests whether the query object debth exceeds maximum depth * @param {*} qryObj an object representation of the query (after destructure) @@ -20,36 +21,39 @@ export default function queryDepthExceeded(queryString: string, queryDepthLimit: * @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 => { - // check if queryObj has query or mutation root type, if so, call query depth check on each of its elements - if (Object.prototype.hasOwnProperty.call(qryObj, 'queries')) { - qryObj.queries?.forEach((element: object) => { - queryDepthCheck(element, queryDepthLimit); - }); - } - if (Object.prototype.hasOwnProperty.call(qryObj, 'mutations')) { - qryObj.mutations?.forEach((element: object) => { - queryDepthCheck(element, queryDepthLimit); - }); - } - - // base case 1: check to see if depth exceeds limit, if so, return error (true => depth has been exceeded) + const queryDepthCheck = (qryObj: queryObj, qryDepthLim: number, depth: number = 0): boolean | undefined => { + // 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; - // Iterate through values of queryObj, and check if it is an object, + // 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 so return invoked QDC with depth+1 + // 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 + }; + }; + // Base case 2: reach end of object keys iteration,return false - depth has not been exceeded return false; }; - // Invoke QDC and if returns true return error, else return nothing - if (queryDepthCheck(queryObj, queryDepthLimit)) { - throw new Error( - 'Security Error: Query depth exceeded maximum query depth limit' - ); - } + + // 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' + ); + }; + }; + }; } diff --git a/test_files/rhum_test_files/DoSSecurity_test.ts b/test_files/rhum_test_files/DoSSecurity_test.ts new file mode 100644 index 0000000..2c2f2f4 --- /dev/null +++ b/test_files/rhum_test_files/DoSSecurity_test.ts @@ -0,0 +1,40 @@ +import { Rhum } from 'https://deno.land/x/rhum@v1.1.4/mod.ts'; +import queryDepthLimiter from '../../src/DoSSecurity.ts'; +import { test } from '../test_variables/DoSSecurity_variables.ts'; + + +Rhum.testPlan('DoSSecurity.ts', () => { + Rhum.testSuite('Query depth limit NOT exceeded tests', () => { + Rhum.testCase('Test query depth with allowable depth 2', () => { + const results = queryDepthLimiter(test.DEPTH_2_QUERY, 10); + Rhum.asserts.assertEquals(undefined, results); + }); + Rhum.testCase('Test mutation with allowable depth of 2', () => { + const results = queryDepthLimiter(test.DEPTH_2_MUTATION, 10); + Rhum.asserts.assertEquals(undefined, results); + }); + }); + + Rhum.testSuite('Query/mutation depth limit IS EXCEEDED tests', () => { + Rhum.testCase('Test query depth should be exceeded', () => { + Rhum.asserts.assertThrows( + () => { + queryDepthLimiter(test.DEPTH_2_QUERY, 1) + }, + Error, + "Security Error: Query depth exceeded maximum query depth limit", + ) + }); + Rhum.testCase('Test mutation depth should be exceeded', () => { + Rhum.asserts.assertThrows( + () => { + queryDepthLimiter(test.DEPTH_2_MUTATION, 1) + }, + Error, + "Security Error: Query depth exceeded maximum mutation depth limit", + ) + }); + }); +}); + +Rhum.run(); \ No newline at end of file diff --git a/test_files/test_variables/DoSSecurity_variables.ts b/test_files/test_variables/DoSSecurity_variables.ts new file mode 100644 index 0000000..d0a638c --- /dev/null +++ b/test_files/test_variables/DoSSecurity_variables.ts @@ -0,0 +1,30 @@ +export const test = { + DEPTH_2_QUERY: ` + query AllActionMovies { + movies(input: { genre: ACTION }) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + }`, + + DEPTH_2_MUTATION: ` + mutation AllActionMoviesAndAllActors { + movies(input: { genre: ACTION }) { + id + title + genre + actors { + id + firstName + lastName + } + } + }`, +}; \ No newline at end of file From 4955bc43b266afd2174222e0368956d5a484beac Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Thu, 24 Jun 2021 11:18:48 -0700 Subject: [PATCH 10/30] Fix caching when directive is false --- src/CacheClassServer.js | 10 +----- src/destructure.js | 24 +++++++++++-- src/lfuBrowserCache.js | 1 + .../test_variables/writeCache_variables.ts | 36 +++++++++++-------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 01a48e8..5119c7b 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -196,7 +196,6 @@ export class Cache { // readVal: { id: "1", title: "Movie-1", releaseYear: 2001 } // for each hash from the input query, build the response object const readVal = await this.cacheRead(hash); - console.log('readVal: ', readVal); // return undefine if hash has been garbage collected if (readVal === undefined) return undefined; if (readVal === 'DELETED') return acc; @@ -205,21 +204,15 @@ export class Cache { if (readVal[field] === 'DELETED') continue; // for each field in the fields input query, add the corresponding value from the cache if the field is not another array of hashs if (readVal[field] === undefined && field !== '__typename') { - console.log('1'); return undefined; } else if (typeof fields[field] !== 'object') { - console.log('2'); // add the typename for the type if (field === '__typename') { - console.log('3'); dataObj[field] = typeName; } else { - console.log('3.5', readVal[field]); - // console.log() dataObj[field] = readVal[field]; } } else { - console.log('4'); // case where the field from the input query is an array of hashes, recursively invoke populateAllHashes // *PROBLEM HERE* - What does populateAllHashes here do? - What is inside releaseYear? // exepected output of calling populateAllHashes: releaseYear : { year: "scalar" } @@ -229,8 +222,6 @@ export class Cache { // readVal: // { id: "1", title: "Movie-1", releaseYear: 2001 } - console.log('readVal[field]: ', readVal[field]); - dataObj[field] = await this.populateAllHashes( [readVal[field]], // <------ wrong type // changed to array type fields[field] @@ -282,4 +273,5 @@ export class Cache { // working for "nested body" queries // Directives -- skip +// Implement skip directives // Write our own tests on Rhum diff --git a/src/destructure.js b/src/destructure.js index 40d8cba..4013473 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -401,11 +401,26 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } if (startDeleteIndex && endDeleteIndex) { + console.log('i at the start: ', queryStr.slice(i)); const directive = queryStr.slice(startDeleteIndex, endDeleteIndex + 2); queryStr = queryStr.replace(directive, ''); + console.log('QUERYSTR AFTER DIRECTIVE REMOVED: ', queryStr); i -= directive.length; - + console.log('i after replacing directive: ', queryStr.slice(i)); + + /* + query Test ($mID: ID, $withRel: Boolean!) { + getMovie(id: $mID) { + id + title + releaseYear { + year + } + } + } + */ + // If directive is false if (!includeQueryField) { let j = i + 3; @@ -425,7 +440,7 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { if (queryStr[k--] === '}') numClosingBrace++; } - const openingBracketIndex = i - 1; + const openingBracketIndex = queryStr.indexOf('{', i); const closingBracketIndex = k + 1; queryStr = queryStr.replace( @@ -441,7 +456,10 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { startFieldNameIndex--; } - queryStr = queryStr.replace(queryStr.slice(startFieldNameIndex, i), ''); + queryStr = queryStr.replace( + queryStr.slice(startFieldNameIndex, i + 1), + '' + ); } // Otherwise, remove the "directive" string, the following body {} if any, diff --git a/src/lfuBrowserCache.js b/src/lfuBrowserCache.js index 3ff7c90..b511ea1 100644 --- a/src/lfuBrowserCache.js +++ b/src/lfuBrowserCache.js @@ -199,6 +199,7 @@ LFUCache.prototype.populateAllHashes = async function ( fields ) { // include the hashname for each hash + console.log('LFUCache.prototype.populateAllHashes ran'); if (!allHashesFromQuery.length) return []; const hyphenIdx = allHashesFromQuery[0].indexOf('~'); const typeName = allHashesFromQuery[0].slice(0, hyphenIdx); diff --git a/test_files/test_variables/writeCache_variables.ts b/test_files/test_variables/writeCache_variables.ts index 880cc60..05d20d3 100644 --- a/test_files/test_variables/writeCache_variables.ts +++ b/test_files/test_variables/writeCache_variables.ts @@ -1,6 +1,13 @@ export const test = { queryStr: ` query AllMoviesAndGetActorById { + getMovie(id: 1) { + id + title + releaseYear { + year + } + } movies { __typename id @@ -203,19 +210,18 @@ export const test = { } } `, - aliasResponse: - { - "data": { - "jediHero": { - "__typename": "Hero", - "id": 2, - "name": "R2-D2", - }, - "empireHero": { - "__typename": "Hero", - "id": 1, - "name": "Luke Skywalker", - } - } -}, + aliasResponse: { + data: { + jediHero: { + __typename: 'Hero', + id: 2, + name: 'R2-D2', + }, + empireHero: { + __typename: 'Hero', + id: 1, + name: 'Luke Skywalker', + }, + }, + }, }; From 1fcc3a0790269044912c13e0032bac7fd428ffef Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 11:24:19 -0700 Subject: [PATCH 11/30] Removed extraneous console log --- src/obsidian.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/obsidian.ts b/src/obsidian.ts index 47e4d9a..3251669 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -68,7 +68,6 @@ export async function ObsidianRouter({ // set redis configurations if (policy || maxmemory) { - // console.log('inside if'); cache.configSet('maxmemory-policy', policy); cache.configSet('maxmemory', maxmemory); } From 96174593b0d7056a79dbc2e92666a322d3687fdd Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 12:05:42 -0700 Subject: [PATCH 12/30] Add co-authors Co-authored-by: kyunglee1 Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: justinwmckay --- src/DoSSecurity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DoSSecurity.ts b/src/DoSSecurity.ts index 12deaa3..e222ca4 100644 --- a/src/DoSSecurity.ts +++ b/src/DoSSecurity.ts @@ -7,7 +7,7 @@ interface queryObj { } /** - * Tests whether a queryString (string representation of query) exceeds the maximum nested depth levels (queryDephtLimit) allowable for the instance of obsidian + * 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 From acf10bf0bcd241b2cd6447bd76d4035ed199958f Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 12:16:38 -0700 Subject: [PATCH 13/30] Fix queryDepthCheck return type (to remove undefined) --- src/DoSSecurity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DoSSecurity.ts b/src/DoSSecurity.ts index e222ca4..b5c0722 100644 --- a/src/DoSSecurity.ts +++ b/src/DoSSecurity.ts @@ -21,7 +21,7 @@ export default function queryDepthLimiter(queryString: string, queryDepthLimit: * @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 | undefined => { + 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, From 86a5844cf26ef0b239d2ce85a8b3dbcbe47f8153 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 12:27:22 -0700 Subject: [PATCH 14/30] Fix import statement on destructure_test.ts to accommodate change in default export in destructure.js --- test_files/rhum_test_files/destructure_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index 8dbc076..aed7f4d 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -1,6 +1,6 @@ import { Rhum } from 'https://deno.land/x/rhum@v1.1.4/mod.ts'; -import { - destructureQueries, +// import destructureQueries from '../../src/destructure.js'; +import destructureQueries, { findQueryStrings, createQueriesObj, splitUpQueryStr, From 09a734c86e00241fd51dc3c70eec4b01cf13db56 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 12:29:49 -0700 Subject: [PATCH 15/30] Fix DoSSecurity depth check level (change from 10 to 2) --- test_files/rhum_test_files/DoSSecurity_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_files/rhum_test_files/DoSSecurity_test.ts b/test_files/rhum_test_files/DoSSecurity_test.ts index 2c2f2f4..4596715 100644 --- a/test_files/rhum_test_files/DoSSecurity_test.ts +++ b/test_files/rhum_test_files/DoSSecurity_test.ts @@ -6,11 +6,11 @@ import { test } from '../test_variables/DoSSecurity_variables.ts'; Rhum.testPlan('DoSSecurity.ts', () => { Rhum.testSuite('Query depth limit NOT exceeded tests', () => { Rhum.testCase('Test query depth with allowable depth 2', () => { - const results = queryDepthLimiter(test.DEPTH_2_QUERY, 10); + const results = queryDepthLimiter(test.DEPTH_2_QUERY, 2); Rhum.asserts.assertEquals(undefined, results); }); Rhum.testCase('Test mutation with allowable depth of 2', () => { - const results = queryDepthLimiter(test.DEPTH_2_MUTATION, 10); + const results = queryDepthLimiter(test.DEPTH_2_MUTATION, 2); Rhum.asserts.assertEquals(undefined, results); }); }); From cc2e07398ae89fb6f69be019565dc979efd10960 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Thu, 24 Jun 2021 15:05:12 -0700 Subject: [PATCH 16/30] Implement support for skip directive Co-authored-by: justinwmckay Co-authored-by: kyunglee1 Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- src/CacheClassServer.js | 3 --- src/destructure.js | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 5119c7b..ecdfee0 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -264,9 +264,6 @@ export class Cache { */ // TO DO: -// - Caching doesn't work with Directives if variable is false only -// - Console log the query objects -// - (query body from the cache needs array bracket to indicate it's retrieved from cache) // - Look into cache tests: // - do any of the test queries they use have nested "bodies" // - YES, but their tests passed but we know that the cache wasnt diff --git a/src/destructure.js b/src/destructure.js index 4013473..978185d 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -377,9 +377,15 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } startIndex = queryStr.indexOf('@'); + let includeQueryField = false; let startDeleteIndex; let endDeleteIndex; + let skipFlag; + //check includes or skip + if (queryStr[startIndex + 1] === 's') { + skipFlag = true; + } // check in between @ and closing parens for (let i = startIndex; i < queryStr.length; i += 1) { @@ -393,10 +399,18 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { endDeleteIndex = i; } - // if directive is true if (startDeleteIndex && char === ':') { + // if directive is true if (queryStr.slice(i, i + 6).indexOf('true') !== -1) { includeQueryField = true; + if (skipFlag) { + includeQueryField = false; + } + // if directive is false + } else { + if (skipFlag) { + includeQueryField = true; + } } } From 170e1a8121014f65a6a6499f33dbebe381566c24 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 15:19:02 -0700 Subject: [PATCH 17/30] Added tests for multiple queries or mutations for DoSSecurity module Co-authored-by: justinwmckay Co-authored-by: kyunglee1 Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- .../rhum_test_files/DoSSecurity_test.ts | 40 ++++++++++-- .../test_variables/DoSSecurity_variables.ts | 64 +++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/test_files/rhum_test_files/DoSSecurity_test.ts b/test_files/rhum_test_files/DoSSecurity_test.ts index 4596715..0ad62f7 100644 --- a/test_files/rhum_test_files/DoSSecurity_test.ts +++ b/test_files/rhum_test_files/DoSSecurity_test.ts @@ -5,18 +5,18 @@ import { test } from '../test_variables/DoSSecurity_variables.ts'; Rhum.testPlan('DoSSecurity.ts', () => { Rhum.testSuite('Query depth limit NOT exceeded tests', () => { - Rhum.testCase('Test query depth with allowable depth 2', () => { + Rhum.testCase('Test query depth of 2 does not exceed allowable depth 2', () => { const results = queryDepthLimiter(test.DEPTH_2_QUERY, 2); Rhum.asserts.assertEquals(undefined, results); }); - Rhum.testCase('Test mutation with allowable depth of 2', () => { + Rhum.testCase('Test mutation depth of 2 does not exceed allowable depth of 2', () => { const results = queryDepthLimiter(test.DEPTH_2_MUTATION, 2); Rhum.asserts.assertEquals(undefined, results); }); }); Rhum.testSuite('Query/mutation depth limit IS EXCEEDED tests', () => { - Rhum.testCase('Test query depth should be exceeded', () => { + Rhum.testCase('Test query depth 2 should exceed depth limit of 1', () => { Rhum.asserts.assertThrows( () => { queryDepthLimiter(test.DEPTH_2_QUERY, 1) @@ -25,7 +25,7 @@ Rhum.testPlan('DoSSecurity.ts', () => { "Security Error: Query depth exceeded maximum query depth limit", ) }); - Rhum.testCase('Test mutation depth should be exceeded', () => { + Rhum.testCase('Test mutation depth 2 should exceed depth limit of 1', () => { Rhum.asserts.assertThrows( () => { queryDepthLimiter(test.DEPTH_2_MUTATION, 1) @@ -35,6 +35,38 @@ Rhum.testPlan('DoSSecurity.ts', () => { ) }); }); + + Rhum.testSuite('Query depth limit NOT exceeded, multiple query tests', () => { + Rhum.testCase('Test multiple queries of depth 2 should not exceed allowable depth 2', () => { + const results = queryDepthLimiter(test.MULTIPLE_DEPTH_2_QUERY, 2); + Rhum.asserts.assertEquals(undefined, results); + }); + Rhum.testCase('Test multiple mutations of depth 2 should not exceed allowable depth 2', () => { + const results = queryDepthLimiter(test.MULTIPLE_DEPTH_2_MUTATION, 2); + Rhum.asserts.assertEquals(undefined, results); + }); + }); + + Rhum.testSuite('Multiple query/mutation depth limit IS EXCEEDED tests', () => { + Rhum.testCase('Test multiple query depth should be exceeded', () => { + Rhum.asserts.assertThrows( + () => { + queryDepthLimiter(test.MULTIPLE_DEPTH_2_QUERY, 1) + }, + Error, + "Security Error: Query depth exceeded maximum query depth limit", + ) + }); + Rhum.testCase('Test multiple mutation depth should be exceeded', () => { + Rhum.asserts.assertThrows( + () => { + queryDepthLimiter(test.MULTIPLE_DEPTH_2_MUTATION, 1) + }, + Error, + "Security Error: Query depth exceeded maximum mutation depth limit", + ) + }); + }); }); Rhum.run(); \ No newline at end of file diff --git a/test_files/test_variables/DoSSecurity_variables.ts b/test_files/test_variables/DoSSecurity_variables.ts index d0a638c..6c9dc3b 100644 --- a/test_files/test_variables/DoSSecurity_variables.ts +++ b/test_files/test_variables/DoSSecurity_variables.ts @@ -27,4 +27,68 @@ export const test = { } } }`, + + MULTIPLE_DEPTH_2_QUERY: ` + query AllActionMovies { + movies(input: { genre: ACTION }) { + __typename + id + title + genre + }, + movies(input: { genre: ACTION }) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + }, + movies(input: { genre: ACTION }) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + }`, + + MULTIPLE_DEPTH_2_MUTATION: ` + mutation AllActionMovies { + movies(input: { genre: ACTION }) { + __typename + id + title + genre + }, + movies(input: { genre: ACTION }) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + }, + movies(input: { genre: ACTION }) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + }`, }; \ No newline at end of file From aac58fcc17e7b07ebefb21a56769b8e60de7deab Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Thu, 24 Jun 2021 15:23:21 -0700 Subject: [PATCH 18/30] Removed extraneous console log --- test_files/rhum_test_files/destructure_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index aed7f4d..fedb1b5 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -1,5 +1,4 @@ import { Rhum } from 'https://deno.land/x/rhum@v1.1.4/mod.ts'; -// import destructureQueries from '../../src/destructure.js'; import destructureQueries, { findQueryStrings, createQueriesObj, From 94871b58f10ef3016a1e9c586cb95eeaacde533d Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Thu, 24 Jun 2021 15:43:28 -0700 Subject: [PATCH 19/30] Add single/multi variable Rhum tests Co-authored-by: justinwmckay Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- .../rhum_test_files/destructure_test.ts | 20 ++ .../test_variables/destructure_variables.ts | 250 ++++++++++++++---- 2 files changed, 221 insertions(+), 49 deletions(-) diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index 8dbc076..b067a67 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -76,4 +76,24 @@ Rhum.testPlan('destructure.ts', () => { }); }); +Rhum.testSuite('destructure single variable query tests', () => { + Rhum.testCase('destructure single variable query string', () => { + const result = destructureQueries( + test.singleVariableTestData, + test.singleVariableTestValue + ); + Rhum.asserts.assertEquals(test.singleVariableTestResult, result); + }); +}); + +Rhum.testSuite('destructure multi variable query tests', () => { + Rhum.testCase('destructure multi variable query', () => { + const result = destructureQueries( + test.multiVariableTestData, + test.multiVariableTestValue + ); + Rhum.asserts.assertEquals(test.multiVariableTestResult, result); + }); +}); + Rhum.run(); diff --git a/test_files/test_variables/destructure_variables.ts b/test_files/test_variables/destructure_variables.ts index 463eedb..db392f6 100644 --- a/test_files/test_variables/destructure_variables.ts +++ b/test_files/test_variables/destructure_variables.ts @@ -224,23 +224,30 @@ export const test = { harrisonActor: actors(id: 00) { firstName lastName`, - findTwoActorsAliasTestResult: { queries: [ { - name: "actors", - alias: "harrisonActor", - arguments: "(id:00)", - fields: { firstName: "scalar", lastName: "scalar", films: { __typename: 'meta', id: 'scalar', title: 'scalar' } } + name: 'actors', + alias: 'harrisonActor', + arguments: '(id:00)', + fields: { + firstName: 'scalar', + lastName: 'scalar', + films: { __typename: 'meta', id: 'scalar', title: 'scalar' }, + }, }, { - name: "actors", - alias: "hammelActor", - arguments: "(id:01)", - fields: { firstName: "scalar", lastName: "scalar", films: { __typename: 'meta', id: 'scalar', title: 'scalar' } } - } - ] + name: 'actors', + alias: 'hammelActor', + arguments: '(id:01)', + fields: { + firstName: 'scalar', + lastName: 'scalar', + films: { __typename: 'meta', id: 'scalar', title: 'scalar' }, + }, + }, + ], }, newAliasTestQuery: ` @@ -253,24 +260,24 @@ export const test = { } }`, - newAliasTestResult:{ + newAliasTestResult: { queries: [ { - name: "hero", - alias: "empireHero", - arguments: "(episode:EMPIRE)", - fields: { name: "scalar" } + name: 'hero', + alias: 'empireHero', + arguments: '(episode:EMPIRE)', + fields: { name: 'scalar' }, }, { - name: "hero", - alias: "jediHero", - arguments: "(episode:JEDI)", - fields: { name: "scalar" } - } - ] - }, + name: 'hero', + alias: 'jediHero', + arguments: '(episode:JEDI)', + fields: { name: 'scalar' }, + }, + ], + }, -fragmentTestData: `query { + fragmentTestData: `query { movies(input: { genre: ACTION }) { __typename id @@ -293,21 +300,31 @@ fragmentTestData: `query { fragment firstAndLast on Actors { firstName lastName - }` , + }`, - fragmentResultData: { + fragmentResultData: { queries: [ { - name: "movies", - arguments: "(input:{genre:ACTION})", - fields: { __typename: "meta", id: "scalar", title: "scalar", genre: "scalar" } + name: 'movies', + arguments: '(input:{genre:ACTION})', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + }, }, { - name: "actors", - arguments: "", - fields: { id: "scalar", films: { __typename: 'meta', id: 'scalar', title: 'scalar' }, firstName: "scalar", lastName: "scalar" } - } - ] + name: 'actors', + arguments: '', + fields: { + id: 'scalar', + films: { __typename: 'meta', id: 'scalar', title: 'scalar' }, + firstName: 'scalar', + lastName: 'scalar', + }, + }, + ], }, fragmentTestData2: `query { @@ -334,24 +351,30 @@ fragmentTestData: `query { fragment firstAndLast on Actors { firstName lastName - }` , + }`, - fragmentResultData2: { + fragmentResultData2: { queries: [ { - name: "movies", - arguments: "(input:{genre:ACTION})", - fields: { __typename: "meta", id: "scalar", title: "scalar", genre: "scalar" , actors:{ - id: "scalar", - films: { __typename: "meta", id: "scalar", title: "scalar" }, - firstName: "scalar", - lastName: "scalar" - }} - }, - ] + name: 'movies', + arguments: '(input:{genre:ACTION})', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + films: { __typename: 'meta', id: 'scalar', title: 'scalar' }, + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + ], }, - fragmentTestData3:` + fragmentTestData3: ` query AllActionMovies { movies(input: { genre: ACTION }) { __typename @@ -371,7 +394,7 @@ fragmentTestData: `query { firstName lastName }`, - + fragmentResultData3: { queries: [ { @@ -390,7 +413,136 @@ fragmentTestData: `query { }, }, ], + }, + + singleVariableTestData: ` + query AllActionMoviesAndAllActors ($movieGenre: String) { + movies(genre: $movieGenre) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + actors { + id + firstName + lastName + films { + __typename + id + title + } + } } -}; + } + `, + singleVariableTestResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + { + name: 'actors', + arguments: '', + fields: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + films: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + }, + }, + }, + ], + }, + + singleVariableTestValue: { + movieGenre: 'ACTION', + }, + multiVariableTestData: ` + query AllActionMoviesAndAllActors ($movieGenre: String, $actorID: ID) { + movies(genre: $movieGenre) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + actors (actor: $actorID) { + id + firstName + lastName + films { + __typename + id + title + } + } + } + } + `, + + multiVariableTestResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + { + name: 'actors', + arguments: '(actor:1)', + fields: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + films: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + }, + }, + }, + ], + }, + + multiVariableTestValue: { + movieGenre: 'ACTION', + actorID: '1', + }, +}; From ed1cc424cbb21977c9402e91ca4077d0926c5a95 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Sat, 26 Jun 2021 11:20:52 -0700 Subject: [PATCH 20/30] Add single @include directive tests Co-authored-by: justinwmckay Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- src/CacheClassServer.js | 7 ++ src/destructure.js | 106 +++++++++++------- .../rhum_test_files/destructure_test.ts | 57 +++++++--- .../test_variables/destructure_variables.ts | 88 +++++++++++++++ 4 files changed, 199 insertions(+), 59 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index ecdfee0..74130a1 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -272,3 +272,10 @@ export class Cache { // Directives -- skip // Implement skip directives // Write our own tests on Rhum +// CTORS INSTEAD OF ACTORS WITH DIRECTIVES +// Test multiple directives +// replace all args not just the first instance nvm +// WRITE CACHE TESTS FAIL + +// * problem with multiple queries +// * false test not working diff --git a/src/destructure.js b/src/destructure.js index 978185d..ece5b68 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -31,11 +31,38 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { // check if query has directives if (queryOperationStr.indexOf('@') !== -1) { + console.log('* * * * * * * * ORIGINAL QUERY STR: ', queryOperationStr); // reassigns query string to handle directives queryOperationStr = destructureQueriesWithDirectives( queryOperationStr, queryOperationVars ); + + // query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boolean!) { movies(genre: $movieGenre) { __typename id title genre actors { id firstName lastName } } + + // query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boole!) { + // movies(genre: ACTION) { + // __typename + // id + // title + // genre { + // id + // firstName + // lastName + // } + // } + // actors { + // id + // firstName + // lastName + // films { + // __typename + // id + // title + // } + // } + // } + console.log('queryStr after directive destructuring: ', queryOperationStr); } // ignore operation name by finding the beginning of the query strings @@ -55,7 +82,7 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { queryOperationVars ); - // console.log('QUERY OBJECT: =================\n', queriesObj); + console.log('QUERY OBJECT: =================\n', queriesObj); return queriesObj; } @@ -344,16 +371,7 @@ export function destructureQueriesWithFragments(queryOperationStr) { } export function destructureQueriesWithDirectives(queryStr, queryVars) { - /* - query Hero($episode: Episode, $withFriends: Boolean!) { - hero(episode: $episode) { - name - friends @include(if: true) { - name - } - } - } -*/ + //find the index of the first closing brace of let startIndex = queryStr.indexOf('{'); let argStartIndex; @@ -370,6 +388,7 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { const newQueryArgs = replaceQueryVariables(oldQueryArgs, queryVars); queryStr = queryStr.replace(oldQueryArgs, newQueryArgs); + console.log('QUERY ARGS AFTER REPLACE: ', newQueryArgs); argStartIndex = undefined; argEndIndex = undefined; @@ -378,15 +397,16 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { startIndex = queryStr.indexOf('@'); - let includeQueryField = false; - let startDeleteIndex; - let endDeleteIndex; let skipFlag; - //check includes or skip if (queryStr[startIndex + 1] === 's') { skipFlag = true; } + let includeQueryField = false; + let startDeleteIndex; + let endDeleteIndex; + //check includes or skip + // check in between @ and closing parens for (let i = startIndex; i < queryStr.length; i += 1) { const char = queryStr[i]; @@ -403,25 +423,15 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { // if directive is true if (queryStr.slice(i, i + 6).indexOf('true') !== -1) { includeQueryField = true; - if (skipFlag) { - includeQueryField = false; - } - // if directive is false - } else { - if (skipFlag) { - includeQueryField = true; - } } } if (startDeleteIndex && endDeleteIndex) { - console.log('i at the start: ', queryStr.slice(i)); const directive = queryStr.slice(startDeleteIndex, endDeleteIndex + 2); queryStr = queryStr.replace(directive, ''); console.log('QUERYSTR AFTER DIRECTIVE REMOVED: ', queryStr); i -= directive.length; - console.log('i after replacing directive: ', queryStr.slice(i)); /* query Test ($mID: ID, $withRel: Boolean!) { @@ -434,33 +444,42 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } */ + + /* + { + id + firstName + lastName + films { + __typename + id + title + } + } + */ // If directive is false if (!includeQueryField) { - let j = i + 3; + console.log('INCLUDE WAS FALSE = = = = = = = = ='); + let startBodyIndex = i + 2; // Delete the body of field - if (queryStr.slice(i, j).indexOf('{') !== -1) { - let numOpeningBrace = 0; - let numClosingBrace = 0; - while (j >= 0) { - if (queryStr[j--] === '{') numOpeningBrace++; - } + // REFACTOR TO LOOK FOR NEXT NON-WS CHAR + if (queryStr.slice(i, i + 3).indexOf('{') !== -1) { + i += 2; + let numOpeningBraces = 1; - // start at the end of the queryStr and count # of closing braces.. - let k = queryStr.length - 1; + while (numOpeningBraces) { + i++; + const char = queryStr[i]; - while (numClosingBrace !== numOpeningBrace) { - if (queryStr[k--] === '}') numClosingBrace++; + if (char === '{') numOpeningBraces++; + if (char === '}') numOpeningBraces--; } - const openingBracketIndex = queryStr.indexOf('{', i); - const closingBracketIndex = k + 1; - - queryStr = queryStr.replace( - queryStr.slice(openingBracketIndex, closingBracketIndex + 1), - '' - ); + // console.log('BODY TO DELETE: ', queryStr.slice(startBodyIndex, ++i)); + const fieldBody = queryStr.slice(startBodyIndex, ++i); + queryStr = queryStr.replace(fieldBody, ''); } // Delete the field with the directive attached to it @@ -484,6 +503,7 @@ export function destructureQueriesWithDirectives(queryStr, queryVars) { } } + console.log('QUERY AFTER DIRECTIVE HANDLING: ', queryStr); return queryStr; } diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index b067a67..d0f4dbc 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -74,25 +74,50 @@ Rhum.testPlan('destructure.ts', () => { Rhum.asserts.assertEquals(test.fragmentResultData3, result); }); }); -}); -Rhum.testSuite('destructure single variable query tests', () => { - Rhum.testCase('destructure single variable query string', () => { - const result = destructureQueries( - test.singleVariableTestData, - test.singleVariableTestValue - ); - Rhum.asserts.assertEquals(test.singleVariableTestResult, result); + // single variable test + Rhum.testSuite('destructure single variable query tests', () => { + Rhum.testCase('destructure single variable query string', () => { + const result = destructureQueries( + test.singleVariableTestData, + test.singleVariableTestValue + ); + Rhum.asserts.assertEquals(test.singleVariableTestResult, result); + }); }); -}); -Rhum.testSuite('destructure multi variable query tests', () => { - Rhum.testCase('destructure multi variable query', () => { - const result = destructureQueries( - test.multiVariableTestData, - test.multiVariableTestValue - ); - Rhum.asserts.assertEquals(test.multiVariableTestResult, result); + // multi variable test + Rhum.testSuite('destructure multi variable query tests', () => { + Rhum.testCase('destructure multi variable query', () => { + const result = destructureQueries( + test.multiVariableTestData, + test.multiVariableTestValue + ); + Rhum.asserts.assertEquals(test.multiVariableTestResult, result); + }); + }); + + // directive test - @include: true + Rhum.testSuite('destructure @include directive query tests', () => { + Rhum.testCase('destructure @include directive (true) query', () => { + const result = destructureQueries( + test.includeDirectiveTestData, + test.includeDirectiveTrueValues + ); + Rhum.asserts.assertEquals(test.includeDirectiveTrueResult, result); + }); + }); + + // directive test - @include: false + Rhum.testSuite('destructure @include directive query tests', () => { + Rhum.testCase('destructure @include directive (false) query', () => { + const result = destructureQueries( + test.includeDirectiveTestData, + test.includeDirectiveFalseValues + ); + + Rhum.asserts.assertEquals(test.includeDirectiveFalseResult, result); + }); }); }); diff --git a/test_files/test_variables/destructure_variables.ts b/test_files/test_variables/destructure_variables.ts index db392f6..e534926 100644 --- a/test_files/test_variables/destructure_variables.ts +++ b/test_files/test_variables/destructure_variables.ts @@ -545,4 +545,92 @@ export const test = { movieGenre: 'ACTION', actorID: '1', }, + + includeDirectiveTestData: `query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boolean!) { + movies(genre: $movieGenre) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + actors @include (if: $withActors) { + id + firstName + lastName + films { + __typename + id + title + } + } + }`, + + includeDirectiveFalseResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + ], + }, + + includeDirectiveFalseValues: { + movieGenre: 'ACTION', + withActors: false, + }, + + includeDirectiveTrueResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + { + name: 'actors', + arguments: '', + fields: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + films: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + }, + }, + }, + ], + }, + + includeDirectiveTrueValues: { + movieGenre: 'ACTION', + withActors: true, + }, }; From 0384f18fbbadea72bc7e80eb802f7d8c42d11787 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Mon, 28 Jun 2021 11:39:09 -0700 Subject: [PATCH 21/30] Fix serverCache_test Co-authored-by: justinwmckay Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- src/CacheClassServer.js | 14 +++++--------- src/normalize.js | 11 +++++++++++ src/obsidian.ts | 2 +- test_files/test_variables/writeCache_variables.ts | 7 ------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 74130a1..9bcb0ab 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -84,8 +84,9 @@ export class Cache { return { data: responseObject }; } - async write(queryStr, queryVars, respObj, deleteFlag) { + async write(queryStr, respObj, deleteFlag, queryVars) { const queryObj = destructureQueries(queryStr, queryVars); + console.log('====== QUERYOBJ ======= : ', queryObj); const resFromNormalize = normalizeResult(queryObj, respObj, deleteFlag); // update the original cache with same reference for (const hash in resFromNormalize) { @@ -269,13 +270,8 @@ export class Cache { // - YES, but their tests passed but we know that the cache wasnt // working for "nested body" queries -// Directives -- skip +// postman try multiple directives +// Write tests for multiple directives // Implement skip directives -// Write our own tests on Rhum -// CTORS INSTEAD OF ACTORS WITH DIRECTIVES -// Test multiple directives -// replace all args not just the first instance nvm // WRITE CACHE TESTS FAIL - -// * problem with multiple queries -// * false test not working +// CTORS INSTEAD OF ACTORS WITH DIRECTIVES?? diff --git a/src/normalize.js b/src/normalize.js index d27419d..44a6315 100644 --- a/src/normalize.js +++ b/src/normalize.js @@ -35,6 +35,8 @@ export default function normalizeResult(queryObj, resultObj, deleteFlag) { // creates a stringified version of query request and stores it in ROOT_QUERY key else if (queryObj.queries || queryObj.mutations) { if (queryObj.queries) { + console.log('LABELID 1'); + result['ROOT_QUERY'] = createRootQuery(queryObj.queries, resultObj); } else { result['ROOT_MUTATION'] = createRootQuery(queryObj.mutations, resultObj); @@ -80,17 +82,26 @@ function createRootQuery(queryObjArr, resultObj) { const args = query.arguments; const queryHash = name + args; const result = resultObj.data[alias] ?? resultObj.data[name]; + console.log('alias: ', alias); + console.log('name: ', name); + console.log('RESULTOBJ: ', resultObj); + // iterate thru the array of current query response // and store the hash of that response in an array if (Array.isArray(result)) { const arrOfHashes = []; result.forEach((obj) => { + console.log('LABELID 2'); + arrOfHashes.push(labelId(obj)); }); //store the array of hashes associated with the queryHash output[queryHash] = arrOfHashes; } else { + console.log('LABELID 3'); + console.log('RESUILT: ', result); + output[queryHash] = [labelId(result)]; } }); diff --git a/src/obsidian.ts b/src/obsidian.ts index b90ac73..e662140 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -113,7 +113,7 @@ export async function ObsidianRouter({ // Normalize response and store in cache // if (useCache && toNormalize && !result.errors) - cache.write(body.query, body.variables, result, false); + cache.write(body.query, result, false, body.variables); var t1 = performance.now(); console.log( 'Obsidian received new data and took ' + (t1 - t0) + ' milliseconds.' diff --git a/test_files/test_variables/writeCache_variables.ts b/test_files/test_variables/writeCache_variables.ts index 05d20d3..f677713 100644 --- a/test_files/test_variables/writeCache_variables.ts +++ b/test_files/test_variables/writeCache_variables.ts @@ -1,13 +1,6 @@ export const test = { queryStr: ` query AllMoviesAndGetActorById { - getMovie(id: 1) { - id - title - releaseYear { - year - } - } movies { __typename id From b76057bde7c3e7c2b1153c4fbe25961385daa8e3 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Mon, 28 Jun 2021 12:18:57 -0700 Subject: [PATCH 22/30] Add @skip directives tests Co-authored-by: justinwmckay Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- .travis.yml | 6 +- src/CacheClassServer.js | 6 +- src/obsidian.ts | 2 +- .../rhum_test_files/destructure_test.ts | 23 +++++ .../test_variables/destructure_variables.ts | 89 +++++++++++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index b010097..c7cdaf0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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= diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 9bcb0ab..527ce6a 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -86,7 +86,6 @@ export class Cache { async write(queryStr, respObj, deleteFlag, queryVars) { const queryObj = destructureQueries(queryStr, queryVars); - console.log('====== QUERYOBJ ======= : ', queryObj); const resFromNormalize = normalizeResult(queryObj, respObj, deleteFlag); // update the original cache with same reference for (const hash in resFromNormalize) { @@ -271,7 +270,10 @@ export class Cache { // working for "nested body" queries // postman try multiple directives +// multi directives doesnt work // Write tests for multiple directives // Implement skip directives -// WRITE CACHE TESTS FAIL +// False directive doesn't write to cache but can read +// -- write to cache doesn't work with variables... (Fix obsidian.ts to solve +// this) // CTORS INSTEAD OF ACTORS WITH DIRECTIVES?? diff --git a/src/obsidian.ts b/src/obsidian.ts index f95b5c5..84088c4 100644 --- a/src/obsidian.ts +++ b/src/obsidian.ts @@ -120,7 +120,7 @@ export async function ObsidianRouter({ // Normalize response and store in cache // if (useCache && toNormalize && !result.errors) { - cache.write(body.query, result, false); + cache.write(body.query, result, false, body.variables); } var t1 = performance.now(); console.log( diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index 15f5cb5..1bd8791 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -120,4 +120,27 @@ Rhum.testPlan('destructure.ts', () => { }); }); +// directive test - @skip: true +Rhum.testSuite('destructure @skip directive query tests', () => { + Rhum.testCase('destructure @skip directive (true) query', () => { + const result = destructureQueries( + test.skipDirectiveTestData, + test.skipDirectiveTrueValues + ); + Rhum.asserts.assertEquals(test.skipDirectiveTrueResult, result); + }); +}); + +// directive test - @skip: false +Rhum.testSuite('destructure @skip directive query tests', () => { + Rhum.testCase('destructure @skip directive (false) query', () => { + const result = destructureQueries( + test.skipDirectiveTestData, + test.skipDirectiveFalseValues + ); + + Rhum.asserts.assertEquals(test.skipDirectiveFalseResult, result); + }); +}); + Rhum.run(); diff --git a/test_files/test_variables/destructure_variables.ts b/test_files/test_variables/destructure_variables.ts index e534926..4aca52b 100644 --- a/test_files/test_variables/destructure_variables.ts +++ b/test_files/test_variables/destructure_variables.ts @@ -633,4 +633,93 @@ export const test = { movieGenre: 'ACTION', withActors: true, }, + + // + skipDirectiveTestData: `query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boolean!) { + movies(genre: $movieGenre) { + __typename + id + title + genre + actors { + id + firstName + lastName + } + } + actors @include (if: $withActors) { + id + firstName + lastName + films { + __typename + id + title + } + } +}`, + + skipDirectiveTrueResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + ], + }, + + skipDirectiveTrueValues: { + movieGenre: 'ACTION', + withActors: false, + }, + + includeDirectiveFalseResult: { + queries: [ + { + name: 'movies', + arguments: '(genre:ACTION)', + fields: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + genre: 'scalar', + actors: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + }, + }, + }, + { + name: 'actors', + arguments: '', + fields: { + id: 'scalar', + firstName: 'scalar', + lastName: 'scalar', + films: { + __typename: 'meta', + id: 'scalar', + title: 'scalar', + }, + }, + }, + ], + }, + + includeDirectiveTrueValues: { + movieGenre: 'ACTION', + withActors: true, + }, }; From 4a91436f091d22f533fda6f9c867b7ba114e98c7 Mon Sep 17 00:00:00 2001 From: Kyung Lee <46550288+kyunglee1@users.noreply.github.com> Date: Mon, 28 Jun 2021 15:23:22 -0700 Subject: [PATCH 23/30] Add comments to destructure.js Co-authored-by: justinwmckay Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- src/CacheClassServer.js | 4 - src/destructure.js | 203 +++++++----------- .../rhum_test_files/destructure_test.ts | 10 +- .../test_variables/destructure_variables.ts | 11 +- 4 files changed, 89 insertions(+), 139 deletions(-) diff --git a/src/CacheClassServer.js b/src/CacheClassServer.js index 527ce6a..3d704df 100644 --- a/src/CacheClassServer.js +++ b/src/CacheClassServer.js @@ -269,10 +269,6 @@ export class Cache { // - YES, but their tests passed but we know that the cache wasnt // working for "nested body" queries -// postman try multiple directives -// multi directives doesnt work -// Write tests for multiple directives -// Implement skip directives // False directive doesn't write to cache but can read // -- write to cache doesn't work with variables... (Fix obsidian.ts to solve // this) diff --git a/src/destructure.js b/src/destructure.js index d333b7e..5ba843c 100644 --- a/src/destructure.js +++ b/src/destructure.js @@ -1,28 +1,23 @@ /** * NOTES: * 1. For now we will record the arguments as a string unless we come up with an alternative argument - * 2. We won't worry about arguments on fields for now * 3. We won't worry about aliases for now - * 4. We won't worry about handling directives for now + * 4. We won't worry about handling MULTIPLE directives for now (both @skip and + * @include) * 5. We won't worry about fragments for now * 6. This function will assume that everything passed in can be a query or a mutation (not both). - * 7. We won't handle variables for now, but we may very well find we need to * 8. We will handle only the meta field "__typename" for now * 9. What edge cases as far as field/query names do we have to worry about: special characters, apostrophes, etc??? - * 10. Directives-implementation don't handle fragment inclusion + * 10. Directives-implementation doesn't handle fragment inclusion * */ // this function will destructure a query/mutation operation string into a query/mutation operation object export function destructureQueries(queryOperationStr, queryOperationVars) { - console.log('================ NEW ====================='); + // Trims blocks of extra white space into a single white space for uniformity + // of incoming queryOperationStrings queryOperationStr = queryOperationStr.replace(/\s+/g, ' ').trim(); - // console.log('QUERY STR WITH REGEX: ', queryOperationStr); - - // query Hero($movieId: ID, $withRel: Boolean!) { getMovie(id: $movieId) { id - // releaseYear @include(if: $withRel) title } } - // check if query has fragments if (queryOperationStr.indexOf('fragment') !== -1) { // reassigns query string to replace fragment references with fragment fields @@ -31,38 +26,11 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { // check if query has directives if (queryOperationStr.indexOf('@') !== -1) { - console.log('* * * * * * * * ORIGINAL QUERY STR: ', queryOperationStr); // reassigns query string to handle directives queryOperationStr = destructureQueriesWithDirectives( queryOperationStr, queryOperationVars ); - - // query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boolean!) { movies(genre: $movieGenre) { __typename id title genre actors { id firstName lastName } } - - // query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boole!) { - // movies(genre: ACTION) { - // __typename - // id - // title - // genre { - // id - // firstName - // lastName - // } - // } - // actors { - // id - // firstName - // lastName - // films { - // __typename - // id - // title - // } - // } - // } - console.log('queryStr after directive destructuring: ', queryOperationStr); } // ignore operation name by finding the beginning of the query strings @@ -82,8 +50,6 @@ export function destructureQueries(queryOperationStr, queryOperationVars) { queryOperationVars ); - console.log('QUERY OBJECT: =================\n', queriesObj); - return queriesObj; } @@ -190,7 +156,9 @@ export function splitUpQueryStr(queryStr, queryVars) { argsString = argsString.replace(/\s/g, ''); // handles edge case where ther are no arguments inside the argument parens pair. if (argsString === '()') argsString = ''; - // TBD: call replaceQueryVariables() + + // if variables were passed in with the query, replace the variables in + // the argString with their values if (queryVars) { argsString = replaceQueryVariables(argsString, queryVars); } @@ -202,50 +170,44 @@ export function splitUpQueryStr(queryStr, queryVars) { } } -/* -query Hero($episode: Episode, $withFriends: Boolean!) { - hero(episode: $episode) { - name - friends @include(if: $withFriends) { - name - } - } -} -*/ - // helper function to manipulate query args string by replacing variables export function replaceQueryVariables(queryArgs, variables) { + // indexes of start ($) & end of variable name let varStartIndex; let varEndIndex; - // Query Arg String: (id:$movieId,title:$title) - // After replacing: (id:2,title:$title) - // output: (id:2, title:"Movie-2") - for (let i = 0; i < queryArgs.length; i += 1) { const char = queryArgs[i]; + // the start of variable names are always indicated by $ if (char === '$') varStartIndex = i; + // the end of variable names are always indicated by , or ) if (char === ',' || char === ')') varEndIndex = i; - // !!!!!! "variables" not necessary here !!!!!!! + // if we have found the start and end positions of a variable in the query + // args string if (varStartIndex && varEndIndex && variables) { + // find the value of that variable by looking it up against the + // "variables" object const varName = queryArgs.slice(varStartIndex + 1, varEndIndex); - // (id: $movieId, title: $title ) const varValue = variables[varName]; + // if the variable was present in the "variables" object, mutate the query + // arg string by replacing the variable with its value if (varValue !== undefined) { queryArgs = queryArgs.replace('$' + varName, varValue); + + // reset i after replacing the variable with its value + /* NOTE: the value of the variable COULD be bigger than the variable itself */ i -= varName.length - varValue.length; - // (id:$movieId,title:$title) i = 11 - // (id:2,title:$title) i = 4 - // varName - varValue } + // reset start and end indexes to look for more variables varStartIndex = undefined; varEndIndex = undefined; } } + return queryArgs; } @@ -370,155 +332,146 @@ export function destructureQueriesWithFragments(queryOperationStr) { return queryCopy; } +// handles query string with directives (@include, @skip) by keeping or omitting +// fields depending on the value of the variable passed in export function destructureQueriesWithDirectives(queryStr, queryVars) { - //find the index of the first closing brace of + // starting point of iteration over queryStr let startIndex = queryStr.indexOf('{'); + // the starting and ending indices of arguments in queryStr let argStartIndex; let argEndIndex; + // iterate over queryStr to replace variables in arguments for (let i = startIndex; i < queryStr.length; i += 1) { const char = queryStr[i]; if (char === '(') argStartIndex = i; if (char === ')') argEndIndex = i; + // if the start and end positions for a query argument have been found, + // replace variables in that argument if (argStartIndex && argEndIndex) { const oldQueryArgs = queryStr.slice(argStartIndex, argEndIndex + 1); const newQueryArgs = replaceQueryVariables(oldQueryArgs, queryVars); queryStr = queryStr.replace(oldQueryArgs, newQueryArgs); - console.log('QUERY ARGS AFTER REPLACE: ', newQueryArgs); + // reset start and end indices to find and replace other arguments argStartIndex = undefined; argEndIndex = undefined; } } + // starting point of iteration is now the first directive (indicated by @) startIndex = queryStr.indexOf('@'); - let skipFlag; + // skipFlag will indicate if the directive is @skip, otherwise @include is + // assumed to be the directive + let skipFlag = false; + if (queryStr[startIndex + 1] === 's') { skipFlag = true; } - let includeQueryField = false; + // Boolean that indicates whether the field to which a directive is attached + // to should be included in or removed from the query string + let includeQueryField; + + // start and end positions of a directive (e.g. --> @include (if: true) <-- ) + /* NOTE: directives (from '@' to the closest closing paren) will always be + deleted from the query string, regardless of whether the value of the variable is true or false */ let startDeleteIndex; let endDeleteIndex; - //check includes or skip - // check in between @ and closing parens + // delete directives from queryStr, as well as the field itself depending + // on the value of the variable in the directive for (let i = startIndex; i < queryStr.length; i += 1) { const char = queryStr[i]; if (char === '@') { startDeleteIndex = i; } - if (char === ')') { endDeleteIndex = i; } + // check value of the variable in the directive (to the right of the ':') if (startDeleteIndex && char === ':') { - // if directive is true + // @skip directives will do the opposite of @include directives if (queryStr.slice(i, i + 6).indexOf('true') !== -1) { - includeQueryField = true; + includeQueryField = skipFlag ? false : true; + } else { + includeQueryField = skipFlag ? true : false; } } + // if the start and end positions for a directive is found, delete it + // (from the '@' to the closest closing paren) if (startDeleteIndex && endDeleteIndex) { const directive = queryStr.slice(startDeleteIndex, endDeleteIndex + 2); queryStr = queryStr.replace(directive, ''); - console.log('QUERYSTR AFTER DIRECTIVE REMOVED: ', queryStr); + + // adjust i after deleting the directive from the queryStr i -= directive.length; - /* - query Test ($mID: ID, $withRel: Boolean!) { - getMovie(id: $mID) { - id - title - releaseYear { - year - } - } - } - */ - - /* - { - id - firstName - lastName - films { - __typename - id - title - } - } - */ - // If directive is false + // if @include directive is false, or if @skip directive is true if (!includeQueryField) { - console.log('INCLUDE WAS FALSE = = = = = = = = ='); + // index of the beginning of a fields body (if the field was of + // non-scalar type and has nested fields) let startBodyIndex = i + 2; - // Delete the body of field + // boolean indicating whether a field has nested fields (more fields + // within '{' and '}') + const hasNestedFields = queryStr.slice(i, i + 3).indexOf('{') !== -1; - // REFACTOR TO LOOK FOR NEXT NON-WS CHAR - if (queryStr.slice(i, i + 3).indexOf('{') !== -1) { + // if a field has nested fields and the @include was false/@skip was + // true, delete the nested fields as well + if (hasNestedFields) { + // adjust i to be pointing inside the body of the field i += 2; - let numOpeningBraces = 1; - while (numOpeningBraces) { + // number of opening curly braces within the body of the field + let numBraces = 1; + + // find the corresponding closing brace for the body of the field with the directive + while (numBraces) { i++; const char = queryStr[i]; - if (char === '{') numOpeningBraces++; - if (char === '}') numOpeningBraces--; + if (char === '{') numBraces++; + if (char === '}') numBraces--; } - // console.log('BODY TO DELETE: ', queryStr.slice(startBodyIndex, ++i)); - const fieldBody = queryStr.slice(startBodyIndex, ++i); + const endBodyIndex = ++i; + + // delete the body of the field + const fieldBody = queryStr.slice(startBodyIndex, endBodyIndex); queryStr = queryStr.replace(fieldBody, ''); } - // Delete the field with the directive attached to it + // delete the field with the directive attached to it let startFieldNameIndex = i - 1; + const endFieldNameIndex = i + 1; while (queryStr[startFieldNameIndex] !== ' ') { startFieldNameIndex--; } queryStr = queryStr.replace( - queryStr.slice(startFieldNameIndex, i + 1), + queryStr.slice(startFieldNameIndex, endFieldNameIndex), '' ); } - // Otherwise, remove the "directive" string, the following body {} if any, - // and the preceding field name - + // reset start and end positions for a directive to look for more directives startDeleteIndex = undefined; endDeleteIndex = undefined; } } - console.log('QUERY AFTER DIRECTIVE HANDLING: ', queryStr); return queryStr; } export default destructureQueries; - -// query Hero($movieId: ID, $withRel: Boolean!) - -// { -// getMovie(id: 1) { -// id -// releaseYear @include(if: true) { -// name { -// student -// } -// } -// title -// } -// } diff --git a/test_files/rhum_test_files/destructure_test.ts b/test_files/rhum_test_files/destructure_test.ts index 1bd8791..d2f0d8c 100644 --- a/test_files/rhum_test_files/destructure_test.ts +++ b/test_files/rhum_test_files/destructure_test.ts @@ -96,7 +96,7 @@ Rhum.testPlan('destructure.ts', () => { }); }); - // directive test - @include: true + // single directive test - @include: true Rhum.testSuite('destructure @include directive query tests', () => { Rhum.testCase('destructure @include directive (true) query', () => { const result = destructureQueries( @@ -107,7 +107,7 @@ Rhum.testPlan('destructure.ts', () => { }); }); - // directive test - @include: false + // single directive test - @include: false Rhum.testSuite('destructure @include directive query tests', () => { Rhum.testCase('destructure @include directive (false) query', () => { const result = destructureQueries( @@ -120,7 +120,7 @@ Rhum.testPlan('destructure.ts', () => { }); }); -// directive test - @skip: true +// single directive test - @skip: true Rhum.testSuite('destructure @skip directive query tests', () => { Rhum.testCase('destructure @skip directive (true) query', () => { const result = destructureQueries( @@ -131,7 +131,7 @@ Rhum.testSuite('destructure @skip directive query tests', () => { }); }); -// directive test - @skip: false +// single directive test - @skip: false Rhum.testSuite('destructure @skip directive query tests', () => { Rhum.testCase('destructure @skip directive (false) query', () => { const result = destructureQueries( @@ -143,4 +143,6 @@ Rhum.testSuite('destructure @skip directive query tests', () => { }); }); +// TO-DO: queries with multiple directives (not just one @include/@skip) + Rhum.run(); diff --git a/test_files/test_variables/destructure_variables.ts b/test_files/test_variables/destructure_variables.ts index 4aca52b..7d6ebce 100644 --- a/test_files/test_variables/destructure_variables.ts +++ b/test_files/test_variables/destructure_variables.ts @@ -634,7 +634,6 @@ export const test = { withActors: true, }, - // skipDirectiveTestData: `query AllActionMoviesAndAllActors ($movieGenre: String, $withActors: Boolean!) { movies(genre: $movieGenre) { __typename @@ -647,7 +646,7 @@ export const test = { lastName } } - actors @include (if: $withActors) { + actors @skip (if: $withActors) { id firstName lastName @@ -681,10 +680,10 @@ export const test = { skipDirectiveTrueValues: { movieGenre: 'ACTION', - withActors: false, + withActors: true, }, - includeDirectiveFalseResult: { + skipDirectiveFalseResult: { queries: [ { name: 'movies', @@ -718,8 +717,8 @@ export const test = { ], }, - includeDirectiveTrueValues: { + skipDirectiveFalseValues: { movieGenre: 'ACTION', - withActors: true, + withActors: false, }, }; From bac39cdda7e057781095046a7756d4584cabd81c Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Mon, 28 Jun 2021 15:40:45 -0700 Subject: [PATCH 24/30] Updated Readme to indicate new features and contributors Co-authored-by: justinwmckay Co-authored-by: kyunglee1 Co-authored-by: raymondcodes Co-authored-by: cssim22 Co-authored-by: pjmsullivan --- README.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7e48098..c6d5f24 100644 --- a/README.md +++ b/README.md @@ -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 +- Full support for GraphQL fragments, directives, and variables +- Optional GraphQL DoS mitigation security module ## Overview @@ -61,10 +63,10 @@ import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; const PORT = 8000; -const app = new Application(); +const app = new Application(https://github.com/); const types = (gql as any)` - // Type definitions + // GraphQL type definitions `; const resolvers = { @@ -82,7 +84,7 @@ const GraphQLRouter = await ObsidianRouter({ redisPort: 6379, }); -const router = new Router(); +const router = new Router(https://github.com/); router.get('/', handlePage); function handlePage(ctx: any) { @@ -102,7 +104,7 @@ function handlePage(ctx: any) { } catch (error) { console.error(error); -app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); +app.use(GraphQLRouter.routes(https://github.com/), GraphQLRouter.allowedMethods(https://github.com/)); await app.listen({ port: PORT }); ``` @@ -112,7 +114,7 @@ await app.listen({ port: PORT }); ```javascript import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; -const App = () => { +const App = (https://github.com/) => { return ( @@ -126,8 +128,8 @@ const App = () => { ```javascript import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; -const MovieApp = () => { - const { query, cache, setCache } = useObsidian(); +const MovieApp = (https://github.com/) => { + const { query, cache, setCache } = useObsidian(https://github.com/); const [movies, setMovies] = (React as any).useState(''); const queryStr = `query { @@ -143,7 +145,7 @@ const MovieApp = () => { return (

{movies}

-

Title: {movie.title}

-

Release Year: {movie.releaseYear}

- - ); -}; - -export default MainContainer; diff --git a/src/server/client.tsx b/src/server/client.tsx deleted file mode 100644 index a200b4b..0000000 --- a/src/server/client.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'https://dev.jspm.io/react'; -import ReactDom from 'https://dev.jspm.io/react-dom'; -import App from './App.tsx'; - -(ReactDom as any).hydrate(, document.getElementById('root')); From c88e53b3106dc0222fa47095507c444a74710d40 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Tue, 29 Jun 2021 12:12:37 -0700 Subject: [PATCH 29/30] Fixed Readme typo --- README.md | 424 +++++++++++++++++++++++++++--------------------------- 1 file changed, 211 insertions(+), 213 deletions(-) diff --git a/README.md b/README.md index 29944b2..532e188 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,211 @@ -![Obsidian](./assets/logoSilver.jpg) - -
GraphQL, built for Deno.
- -
- -

- Obsidian - Tweet -

- -

from Lascaux

- -
- -

- GitHub - GitHub issues - GitHub last commit - GitHub Repo stars -

- -## Features - -- 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 -- Full support for GraphQL fragments, directives, and variables -- Optional GraphQL DoS attack mitigation security module - -## Overview - -Obsidian is Deno's first native GraphQL caching client and server module. Boasting lightning-fast caching and fetching capabilities alongside headlining normalization and destructuring strategies, Obsidian is equipped to support scalable, highly performant applications. - -Optimized for use in server-side rendered React apps built with Deno, full stack integration of Obsidian enables many of its most powerful features, including optimized caching exchanges between client and server and extremely lightweight client-side caching. - -## Documentation and Demo - -[obsidian.land](http://obsidian.land) - -## Installation - -
QUICK START
-
- -In the server: - -```javascript -import { ObsidianRouter } from 'https://deno.land/x/obsidian/mod.ts'; -``` - -In the app: - -```javascript -import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; -``` - -## Creating the Router - -```javascript -import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; -import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; - -const PORT = 8000; - -const app = new Application(); - -const types = (gql as any)` - // GraphQL type definitions -`; - -const resolvers = { - // Resolvers -} - -interface ObsRouter extends Router { - obsidianSchema?: any; -} - -const GraphQLRouter = await ObsidianRouter({ - Router, - typeDefs: types, - resolvers: resolvers, - redisPort: 6379, -}); - -const router = new Router(); -router.get('/', handlePage); - -function handlePage(ctx: any) { - try { - const body = (ReactDomServer as any).renderToString(); - ctx.response.body = ` - - - - SSR React App - - -
\${body}
- - - `; - } catch (error) { - console.error(error); - -app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); - -await app.listen({ port: PORT }); -``` - -## Creating the Wrapper - -```javascript -import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const App = () => { - return ( - - - - ); -}; -``` - -## Making a Query - -```javascript -import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const MovieApp = () => { - const { query, cache, setCache } = useObsidian(); - const [movies, setMovies] = (React as any).useState(''); - - const queryStr = `query { - movies { - id - title - releaseYear - genre - } - } - `; - - return ( -

{movies}

- - ); -}; -``` - -## Making a Mutation - -```javascript -import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const MovieApp = () => { - const { mutate, cache, setCache } = useObsidian(); - const [movies, setMovies] = (React as any).useState(''); - - const queryStr = `mutation { - addMovie(input: {title: "Cruel Intentions", releaseYear: 1999, genre: "DRAMA" }) { - id - title - releaseYear - genre - } - } - `; - - return ( -

{movies}

- - ); -} -``` - -## Documentation - -[obsidian.land](http://obsidian.land) - -## Authors - -_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) -[Travis Frank](https://github.com/TravisFrankMTG/) -[Lourent Flores](https://github.com/lourentflores) -[Esma Sahraoui](https://github.com/EsmaShr) -[Derek Miller](https://github.com/dsymiller) -[Eric Marcatoma](https://github.com/ericmarc159) -[Spencer Stockton](https://github.com/tonstock) +![Obsidian](./assets/logoSilver.jpg) + +
GraphQL, built for Deno.
+ +
+ +

+ Obsidian + Tweet +

+ +

from Lascaux

+ +
+ +

+ GitHub + GitHub issues + GitHub last commit + GitHub Repo stars +

+ +## Features + +- 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 + +Obsidian is Deno's first native GraphQL caching client and server module. Boasting lightning-fast caching and fetching capabilities alongside headlining normalization and destructuring strategies, Obsidian is equipped to support scalable, highly performant applications. + +Optimized for use in server-side rendered React apps built with Deno, full stack integration of Obsidian enables many of its most powerful features, including optimized caching exchanges between client and server and extremely lightweight client-side caching. + +## Documentation and Demo + +[obsidian.land](http://obsidian.land) + +## Installation + +
QUICK START
+
+ +In the server: + +```javascript +import { ObsidianRouter } from 'https://deno.land/x/obsidian/mod.ts'; +``` + +In the app: + +```javascript +import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; +``` + +## Creating the Router + +```javascript +import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; +import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; + +const PORT = 8000; + +const app = new Application(); + +const types = (gql as any)` + // GraphQL type definitions +`; + +const resolvers = { + // Resolvers +} + +interface ObsRouter extends Router { + obsidianSchema?: any; +} + +const GraphQLRouter = await ObsidianRouter({ + Router, + typeDefs: types, + resolvers: resolvers, + redisPort: 6379, +}); + +const router = new Router(); +router.get('/', handlePage); + +function handlePage(ctx: any) { + try { + const body = (ReactDomServer as any).renderToString(); + ctx.response.body = ` + + + + SSR React App + + +
\${body}
+ + + `; + } catch (error) { + console.error(error); + +app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); + +await app.listen({ port: PORT }); +``` + +## Creating the Wrapper + +```javascript +import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const App = () => { + return ( + + + + ); +}; +``` + +## Making a Query + +```javascript +import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const MovieApp = () => { + const { query, cache, setCache } = useObsidian(); + const [movies, setMovies] = (React as any).useState(''); + + const queryStr = `query { + movies { + id + title + releaseYear + genre + } + } + `; + + return ( +

{movies}

+ + ); +}; +``` + +## Making a Mutation + +```javascript +import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const MovieApp = () => { + const { mutate, cache, setCache } = useObsidian(); + const [movies, setMovies] = (React as any).useState(''); + + const queryStr = `mutation { + addMovie(input: {title: "Cruel Intentions", releaseYear: 1999, genre: "DRAMA" }) { + id + title + releaseYear + genre + } + } + `; + + return ( +

{movies}

+ + ); +} +``` + +## Documentation + +[obsidian.land](http://obsidian.land) + +## Authors + +_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) +[Travis Frank](https://github.com/TravisFrankMTG/) +[Lourent Flores](https://github.com/lourentflores) +[Esma Sahraoui](https://github.com/EsmaShr) +[Derek Miller](https://github.com/dsymiller) +[Eric Marcatoma](https://github.com/ericmarc159) +[Spencer Stockton](https://github.com/tonstock) From 5f5808b80369982e3ed1e36b9645e4c9d2bd1236 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Tue, 29 Jun 2021 12:14:33 -0700 Subject: [PATCH 30/30] Fixed Readme typo --- README.md | 422 +++++++++++++++++++++++++++--------------------------- 1 file changed, 211 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index 532e188..9f2c6f4 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,211 @@ -![Obsidian](./assets/logoSilver.jpg) - -
GraphQL, built for Deno.
- -
- -

- Obsidian - Tweet -

- -

from Lascaux

- -
- -

- GitHub - GitHub issues - GitHub last commit - GitHub Repo stars -

- -## Features - -- 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 - -Obsidian is Deno's first native GraphQL caching client and server module. Boasting lightning-fast caching and fetching capabilities alongside headlining normalization and destructuring strategies, Obsidian is equipped to support scalable, highly performant applications. - -Optimized for use in server-side rendered React apps built with Deno, full stack integration of Obsidian enables many of its most powerful features, including optimized caching exchanges between client and server and extremely lightweight client-side caching. - -## Documentation and Demo - -[obsidian.land](http://obsidian.land) - -## Installation - -
QUICK START
-
- -In the server: - -```javascript -import { ObsidianRouter } from 'https://deno.land/x/obsidian/mod.ts'; -``` - -In the app: - -```javascript -import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; -``` - -## Creating the Router - -```javascript -import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; -import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; - -const PORT = 8000; - -const app = new Application(); - -const types = (gql as any)` - // GraphQL type definitions -`; - -const resolvers = { - // Resolvers -} - -interface ObsRouter extends Router { - obsidianSchema?: any; -} - -const GraphQLRouter = await ObsidianRouter({ - Router, - typeDefs: types, - resolvers: resolvers, - redisPort: 6379, -}); - -const router = new Router(); -router.get('/', handlePage); - -function handlePage(ctx: any) { - try { - const body = (ReactDomServer as any).renderToString(); - ctx.response.body = ` - - - - SSR React App - - -
\${body}
- - - `; - } catch (error) { - console.error(error); - -app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); - -await app.listen({ port: PORT }); -``` - -## Creating the Wrapper - -```javascript -import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const App = () => { - return ( - - - - ); -}; -``` - -## Making a Query - -```javascript -import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const MovieApp = () => { - const { query, cache, setCache } = useObsidian(); - const [movies, setMovies] = (React as any).useState(''); - - const queryStr = `query { - movies { - id - title - releaseYear - genre - } - } - `; - - return ( -

{movies}

- - ); -}; -``` - -## Making a Mutation - -```javascript -import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; - -const MovieApp = () => { - const { mutate, cache, setCache } = useObsidian(); - const [movies, setMovies] = (React as any).useState(''); - - const queryStr = `mutation { - addMovie(input: {title: "Cruel Intentions", releaseYear: 1999, genre: "DRAMA" }) { - id - title - releaseYear - genre - } - } - `; - - return ( -

{movies}

- - ); -} -``` - -## Documentation - -[obsidian.land](http://obsidian.land) - -## Authors - -_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) -[Travis Frank](https://github.com/TravisFrankMTG/) -[Lourent Flores](https://github.com/lourentflores) -[Esma Sahraoui](https://github.com/EsmaShr) -[Derek Miller](https://github.com/dsymiller) -[Eric Marcatoma](https://github.com/ericmarc159) -[Spencer Stockton](https://github.com/tonstock) +![Obsidian](./assets/logoSilver.jpg) + +
GraphQL, built for Deno.
+ +
+ +

+ Obsidian + Tweet +

+ +

from Lascaux

+ +
+ +

+ GitHub + GitHub issues + GitHub last commit + GitHub Repo stars +

+ +## Features + +- 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 + +Obsidian is Deno's first native GraphQL caching client and server module. Boasting lightning-fast caching and fetching capabilities alongside headlining normalization and destructuring strategies, Obsidian is equipped to support scalable, highly performant applications. + +Optimized for use in server-side rendered React apps built with Deno, full stack integration of Obsidian enables many of its most powerful features, including optimized caching exchanges between client and server and extremely lightweight client-side caching. + +## Documentation and Demo + +[obsidian.land](http://obsidian.land) + +## Installation + +
QUICK START
+
+ +In the server: + +```javascript +import { ObsidianRouter } from 'https://deno.land/x/obsidian/mod.ts'; +``` + +In the app: + +```javascript +import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; +``` + +## Creating the Router + +```javascript +import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; +import { ObsidianRouter, gql } from 'https://deno.land/x/obsidian/mod.ts'; + +const PORT = 8000; + +const app = new Application(); + +const types = (gql as any)` + // GraphQL type definitions +`; + +const resolvers = { + // Resolvers +} + +interface ObsRouter extends Router { + obsidianSchema?: any; +} + +const GraphQLRouter = await ObsidianRouter({ + Router, + typeDefs: types, + resolvers: resolvers, + redisPort: 6379, +}); + +const router = new Router(); +router.get('/', handlePage); + +function handlePage(ctx: any) { + try { + const body = (ReactDomServer as any).renderToString(); + ctx.response.body = ` + + + + SSR React App + + +
\${body}
+ + + `; + } catch (error) { + console.error(error); + +app.use(GraphQLRouter.routes(), GraphQLRouter.allowedMethods()); + +await app.listen({ port: PORT }); +``` + +## Creating the Wrapper + +```javascript +import { ObsidianWrapper } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const App = () => { + return ( + + + + ); +}; +``` + +## Making a Query + +```javascript +import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const MovieApp = () => { + const { query, cache, setCache } = useObsidian(); + const [movies, setMovies] = (React as any).useState(''); + + const queryStr = `query { + movies { + id + title + releaseYear + genre + } + } + `; + + return ( +

{movies}

+ + ); +}; +``` + +## Making a Mutation + +```javascript +import { useObsidian, BrowserCache } from 'https://deno.land/x/obsidian/clientMod.ts'; + +const MovieApp = () => { + const { mutate, cache, setCache } = useObsidian(); + const [movies, setMovies] = (React as any).useState(''); + + const queryStr = `mutation { + addMovie(input: {title: "Cruel Intentions", releaseYear: 1999, genre: "DRAMA" }) { + id + title + releaseYear + genre + } + } + `; + + return ( +

{movies}

+ + ); +} +``` + +## Documentation + +[obsidian.land](http://obsidian.land) + +## Authors + +_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) +[Travis Frank](https://github.com/TravisFrankMTG/) +[Lourent Flores](https://github.com/lourentflores) +[Esma Sahraoui](https://github.com/EsmaShr) +[Derek Miller](https://github.com/dsymiller) +[Eric Marcatoma](https://github.com/ericmarc159) +[Spencer Stockton](https://github.com/tonstock)