├── .gitignore ├── Parser ├── anagraphCreator.js ├── parsertests │ ├── anagraphCreator.test.js │ ├── queryParser.test.js │ ├── stringProperties.test.js │ └── stringValidator.test.js ├── queryParser.js ├── queryValidator.js ├── ruleValidator.js ├── schemaParser.js ├── stringProperties.js ├── stringValidator.js └── tests │ ├── anagraphCreator.test.js │ ├── queryParser.test.js │ ├── stringProperties.test.js │ └── stringValidator.test.js ├── README.md ├── anagraphqlEditor ├── .eslintrc.json ├── buildRun.sh ├── dist │ ├── IMG_0672.jpg │ ├── index.html │ └── stylesheet.css ├── package-lock.json ├── package.json ├── src │ ├── client │ │ ├── actions │ │ │ └── actions.js │ │ ├── components │ │ │ ├── App.jsx │ │ │ ├── ApplicableRules.jsx │ │ │ ├── CodeBlock.jsx │ │ │ ├── CodeContainer.jsx │ │ │ ├── CodeEditor.jsx │ │ │ ├── Collapsible.jsx │ │ │ ├── Graph.jsx │ │ │ ├── Headline.jsx │ │ │ ├── History.jsx │ │ │ ├── JsonDisplay.jsx │ │ │ ├── PoliciesContainer.jsx │ │ │ ├── PolicySelector.jsx │ │ │ ├── RulesConfiguration.jsx │ │ │ ├── RulesDisplay.jsx │ │ │ ├── SideBar.jsx │ │ │ └── Visualizer.jsx │ │ ├── constants │ │ │ └── actionTypes.js │ │ ├── index.jsx │ │ ├── play.svg │ │ ├── reducers │ │ │ ├── queryReducer.js │ │ │ ├── responseReducer.js │ │ │ ├── rootReducer.js │ │ │ └── rulesReducer.js │ │ ├── store.js │ │ └── utility │ │ │ ├── d3DataCreator.js │ │ │ └── introspectionQueries.js │ └── server │ │ └── server.js └── webpack.config.js ├── buildRun.sh ├── docs └── index.html ├── graphqlTestServer ├── .eslintrc ├── async_db.js ├── buildRun.sh ├── config │ ├── asyncFakeData.js │ ├── config.sh │ ├── db.sql │ └── fakeData.js ├── database.js ├── db_connectStr.js ├── index.js ├── package-lock.json ├── package.json ├── resolvers.js └── schema.js ├── index.js ├── package-lock.json ├── package.json └── renderGraphiql.js /.gitignore: -------------------------------------------------------------------------------- 1 | anagraphqlEditor/node_modules/ 2 | graphqlTestServer/node_modules/ 3 | bundle.js 4 | node_modules -------------------------------------------------------------------------------- /Parser/anagraphCreator.js: -------------------------------------------------------------------------------- 1 | const parser = require('./queryParser'); 2 | const stringProperties = require('./stringProperties'); 3 | 4 | 5 | // Exporting an anonyous function that takes one param, a query, and returns the anagraph object. 6 | 7 | // This file exports the anagraph object. The anagraph object contain values that are the result of passing the incoming query through the parser function and then passing that result (theOBJ) through the stringProperties function. 8 | 9 | 10 | const anagraphCreator = (query) => { 11 | const theOBJ = parser(query); 12 | const anagraph = {}; 13 | anagraph.analytics = stringProperties.countResolvers(theOBJ); 14 | anagraph.analytics.maxNested = stringProperties.countDepth(query); 15 | anagraph.analytics.totalResolvers = stringProperties.combinedResolvers(anagraph); 16 | anagraph.analytics.totalFields = stringProperties.combinedFields(anagraph); 17 | return anagraph; 18 | }; 19 | 20 | module.exports = anagraphCreator; 21 | -------------------------------------------------------------------------------- /Parser/parsertests/anagraphCreator.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | Anagraph Object tests 3 | ************/ 4 | const anagraphCreator = require('../anagraphCreator') 5 | 6 | const sampleQuery = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | const anagraph = anagraphCreator(sampleQuery) 8 | 9 | describe('anagraphCreator', () => { 10 | 11 | test('is a function', () => { 12 | expect(typeof anagraphCreator).toBe('function'); 13 | }) 14 | 15 | test('returns an object', () => { 16 | expect(typeof anagraphCreator(sampleQuery)).toBe('object'); 17 | }) 18 | 19 | test('has a property called analytics', () => { 20 | expect(anagraph).toHaveProperty('analytics') 21 | }) 22 | 23 | test('analytics property has a property called shallowResolvers', () => { 24 | expect(anagraph.analytics).toHaveProperty('shallowResolvers') 25 | }) 26 | 27 | test('analytics property has a property called specificResolvers', () => { 28 | expect(anagraph.analytics).toHaveProperty('specificResolvers') 29 | }) 30 | 31 | test('analytics property has a property called fields', () => { 32 | expect(anagraph.analytics).toHaveProperty('fields') 33 | }) 34 | 35 | test('analytics property has a property called maxNested', () => { 36 | expect(anagraph.analytics).toHaveProperty('maxNested') 37 | }) 38 | 39 | test('analytics property has a property called totalFields', () => { 40 | expect(anagraph.analytics).toHaveProperty('totalFields') 41 | }) 42 | 43 | test('the value of anagraph.analytics.shallowResolvers is an object', () => { 44 | expect(typeof anagraph.analytics.shallowResolvers).toBe('object') 45 | }) 46 | 47 | test('the value of anagraph.analytics.specificResolvers is an number', () => { 48 | expect(typeof anagraph.analytics.specificResolvers).toBe('object') 49 | }) 50 | 51 | test('the value of anagraph.analytics.fields is an object', () => { 52 | expect(typeof anagraph.analytics.fields).toBe('object') 53 | }) 54 | 55 | test('the value of anagraph.analytics.maxNested is an number', () => { 56 | expect(typeof anagraph.analytics.maxNested).toBe('number') 57 | }) 58 | 59 | test('the value of anagraph.analytics.totalFields is an number', () => { 60 | expect(typeof anagraph.analytics.totalFields).toBe('number') 61 | }) 62 | 63 | 64 | 65 | 66 | }) 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Parser/parsertests/queryParser.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | Query Parser tests 3 | ************/ 4 | const queryParser = require('../queryParser') 5 | const sampleQuery = '{ author { firstname }}' 6 | 7 | describe('queryParser', () => { 8 | test('is a function', () => { 9 | expect(typeof queryParser).toBe('function') 10 | }) 11 | test('returns an object', () => { 12 | expect(typeof queryParser(sampleQuery)).toBe('object'); 13 | }) 14 | 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /Parser/parsertests/stringProperties.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | stringProperties tests 3 | ************/ 4 | const stringProperties = require('../stringProperties') 5 | 6 | const sampleString = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | 8 | describe('stringProperties', () => { 9 | 10 | test('should throw an error on an empty string', () => { 11 | const emptyString = () => stringProperties(''); 12 | expect(emptyString).toThrow(TypeError); 13 | }); 14 | 15 | test('is an object', () => { 16 | expect(typeof stringProperties).toBe('object'); 17 | }) 18 | 19 | test('has a property called lengthOfString', () => { 20 | expect(stringProperties).toHaveProperty('lengthOfString') 21 | }) 22 | 23 | test('has a property called countResolvers', () => { 24 | expect(stringProperties).toHaveProperty('countResolvers') 25 | }) 26 | 27 | test('has a property called countDepth', () => { 28 | expect(stringProperties).toHaveProperty('countDepth') 29 | }) 30 | test('has a property called combinedResolvers', () => { 31 | expect(stringProperties).toHaveProperty('combinedResolvers') 32 | }) 33 | 34 | test('has a property called combinedFields', () => { 35 | expect(stringProperties).toHaveProperty('combinedFields') 36 | }) 37 | 38 | 39 | 40 | 41 | }) 42 | 43 | 44 | -------------------------------------------------------------------------------- /Parser/parsertests/stringValidator.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | stringProperties tests 3 | ************/ 4 | const stringValidator = require('../stringValidator') 5 | 6 | const sampleString = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | 8 | describe('stringValidator.balancedParentes', () => { 9 | test('is a function', () => { 10 | expect(typeof stringValidator.balancedParentes).toBe('function') 11 | }) 12 | test('should return a boolean', () => { 13 | expect(typeof stringValidator.balancedParentes(sampleString)).toBe('boolean') 14 | }) 15 | 16 | 17 | }) 18 | -------------------------------------------------------------------------------- /Parser/queryParser.js: -------------------------------------------------------------------------------- 1 | 2 | const parser = (q) => { 3 | const formatted = q.replace(/ *\([^)]*\) */g, '') 4 | .slice(1, -1) 5 | .replace(/(\s*{)/g, '{') 6 | .split(/\s/g) 7 | .filter(cv => cv !== ''); 8 | let i = 0; 9 | const arr = formatted.slice(); 10 | const helper = () => { 11 | const test = {}; 12 | while (arr[i] && arr[i] !== '}') { 13 | if (arr[i] && arr[i][arr[i].length - 1] === '{') { 14 | test[arr[i++].slice(0, -1)] = helper(); 15 | } else if (arr[i] !== '}') { 16 | test[arr[i]] = 'val'; 17 | } 18 | i += 1; 19 | } 20 | return test; 21 | }; 22 | return helper(); 23 | }; 24 | 25 | 26 | module.exports = parser; 27 | -------------------------------------------------------------------------------- /Parser/queryValidator.js: -------------------------------------------------------------------------------- 1 | module.exports = (anagraph, rules) => { 2 | // console.log(anagraph); 3 | // console.log(rules); 4 | const queryValidation = {}; 5 | 6 | const keyRules = Object.keys(rules); 7 | 8 | for (let i = 0; i < keyRules.length; i += 1) { 9 | if (typeof rules[keyRules[i]] !== 'object') { 10 | if (anagraph.analytics[keyRules[i]] > rules[keyRules[i]]) { 11 | queryValidation.error = (`${[keyRules[i]]} - Rule violation!`); 12 | return queryValidation; 13 | } 14 | } 15 | } 16 | 17 | if (rules.shallowResolvers) { 18 | const ruleShallowResolvers = Object.keys(rules.shallowResolvers); 19 | for (let i = 0; i < ruleShallowResolvers.length; i += 1) { 20 | if (anagraph.analytics.shallowResolvers[ruleShallowResolvers[i]] > rules.shallowResolvers[ruleShallowResolvers[i]]) { 21 | queryValidation.error = (`${[ruleShallowResolvers[i]]} - shallowResolvers rule violation!`); 22 | return queryValidation; 23 | } 24 | } 25 | } 26 | 27 | return queryValidation; 28 | }; 29 | -------------------------------------------------------------------------------- /Parser/ruleValidator.js: -------------------------------------------------------------------------------- 1 | module.exports = (applicableRules, rules) => { 2 | // console.log(applicableRules, ' applicableRules'); 3 | 4 | const validateRules = {}; 5 | 6 | const keyRules = Object.keys(rules); 7 | 8 | for (let i = 0; i < keyRules.length; i += 1) { 9 | if (!applicableRules.hasOwnProperty(keyRules[i])) { 10 | validateRules.error = `${keyRules[i]} is not a valid rule`; 11 | return validateRules; 12 | } 13 | } 14 | 15 | if (rules.hasOwnProperty('shallowResolvers')) { 16 | const rulesShallowResolvers = Object.keys(rules.shallowResolvers); 17 | for (let i = 0; i < rulesShallowResolvers.length; i += 1) { 18 | if (!applicableRules.shallowResolvers.hasOwnProperty(rulesShallowResolvers[i])) { 19 | validateRules.error = `${rulesShallowResolvers[i]} is not a shallowResolver rule`; 20 | return validateRules; 21 | } 22 | } 23 | } 24 | 25 | if (rules.hasOwnProperty('specificResolvers')) { 26 | const rulesSpecificResolvers = Object.keys(rules.specificResolvers); 27 | for (let i = 0; i < rulesSpecificResolvers.length; i += 1) { 28 | if (!applicableRules.specificResolvers.hasOwnProperty(rulesSpecificResolvers[i])) { 29 | validateRules.error = `${rulesSpecificResolvers[i]} is not a shallowResolver rule`; 30 | return validateRules; 31 | } 32 | } 33 | } 34 | 35 | return validateRules; 36 | }; 37 | -------------------------------------------------------------------------------- /Parser/schemaParser.js: -------------------------------------------------------------------------------- 1 | const { introspectionFromSchema } = require('../graphqlTestServer/node_modules/graphql'); 2 | 3 | module.exports = (schema) => { 4 | const defaultTypes = { 5 | // RootQueryType: true, 6 | ID: true, 7 | String: true, 8 | Int: true, 9 | __Schema: true, 10 | __Type: true, 11 | __TypeKind: true, 12 | Boolean: true, 13 | __Field: true, 14 | __InputValue: true, 15 | __EnumValue: true, 16 | __Directive: true, 17 | __DirectiveLocation: true, 18 | }; 19 | 20 | const sure = introspectionFromSchema(schema); 21 | const jsonifiedSchema = JSON.parse(JSON.stringify(sure.__schema.types)); 22 | 23 | const schemaRules = { 24 | fields: {}, 25 | shallowResolvers: {}, 26 | specificResolvers: {}, 27 | maxNested: true, 28 | totalResolvers: true, 29 | totalFields: true, 30 | }; 31 | 32 | for (let i = 0; i < jsonifiedSchema.length; i += 1) { 33 | if (!defaultTypes.hasOwnProperty(jsonifiedSchema[i].name)) { 34 | if (jsonifiedSchema[i].name === 'RootQueryType') { 35 | for (let z = 0; z < jsonifiedSchema[i].fields.length; z += 1) { 36 | schemaRules.specificResolvers[`${jsonifiedSchema[i].name}_${jsonifiedSchema[i].fields[z].name}`] = true; 37 | schemaRules.shallowResolvers[jsonifiedSchema[i].fields[z].name] = true; 38 | } 39 | } 40 | for (let j = 0; j < jsonifiedSchema[i].fields.length; j += 1) { 41 | if (jsonifiedSchema[i].fields[j].type.kind === 'SCALAR') { 42 | schemaRules.fields[jsonifiedSchema[i].name] = true; 43 | } 44 | if (jsonifiedSchema[i].fields[j].type.kind !== 'SCALAR') { 45 | if (jsonifiedSchema[i].name !== 'RootQueryType') { 46 | schemaRules.shallowResolvers[jsonifiedSchema[i].name] = true; 47 | schemaRules.specificResolvers[`${jsonifiedSchema[i].name}_${jsonifiedSchema[i].fields[j].name}`] = true; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | return schemaRules; 54 | }; 55 | -------------------------------------------------------------------------------- /Parser/stringProperties.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lengthOfString: string => string.length, 3 | maxNested: 0, 4 | currentNested: 0, 5 | 6 | countResolvers: (object) => { 7 | const analytics = { 8 | specificResolvers: {}, 9 | shallowResolvers: {}, 10 | fields: {}, 11 | }; 12 | const recursiveObj = (object, strName, number = 0) => { 13 | const keys = Object.keys(object); 14 | for (let i = 0; i < keys.length; i += 1) { 15 | if (typeof object[keys[i]] === 'object') { 16 | if (analytics.specificResolvers.hasOwnProperty(`${strName}_${keys[i]}`)) { 17 | analytics.specificResolvers[`${strName}_${keys[i]}`] += 1; 18 | } else { 19 | analytics.specificResolvers[`${strName}_${keys[i]}`] = 1; 20 | } 21 | if (analytics.shallowResolvers.hasOwnProperty(keys[i])) { 22 | analytics.shallowResolvers[keys[i]] += 1; 23 | } else { 24 | analytics.shallowResolvers[keys[i]] = 1; 25 | } 26 | recursiveObj(object[keys[i]], keys[i]); 27 | } else if (analytics.fields.hasOwnProperty(keys[i])) { 28 | analytics.fields[keys[i]] += 1; 29 | } else { 30 | analytics.fields[keys[i]] = 1; 31 | } 32 | } 33 | }; 34 | recursiveObj(object, 'RootQueryType'); 35 | return analytics; 36 | }, 37 | 38 | countDepth: (query) => { 39 | let num = -1; 40 | let max = -1; 41 | for (let i = 0; i < query.length; i += 1) { 42 | if (query[i] === '{') num += 1; 43 | if (query[i] === '}') num -= 1; 44 | if (num > max) max = num; 45 | } 46 | return max; 47 | }, 48 | combinedResolvers: (obj) => { 49 | const arr = Object.values(obj.analytics.shallowResolvers); 50 | return arr.reduce((acc, ite) => acc += ite); 51 | }, 52 | 53 | combinedFields: (obj) => { 54 | const arr = Object.values(obj.analytics.fields); 55 | return arr.reduce((acc, ite) => acc += ite); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /Parser/stringValidator.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | firstAndLastParentes: string => (string[0] === '{' && string[string.length - 1] === '}'), 4 | balancedParentes: (string) => { 5 | const bracks = string.match(/[{}[\]()]/g, '').join(''); 6 | const stack = []; 7 | const map = { 8 | '(': ')', 9 | '[': ']', 10 | '{': '}', 11 | }; 12 | for (let i = 0; i < bracks.length; i += 1) { 13 | if (bracks[i] === '(' || bracks[i] === '{' || bracks[i] === '[') { 14 | stack.push(bracks[i]); 15 | } else { 16 | const last = stack.pop(); 17 | if (bracks[i] !== map[last]) return false; 18 | } 19 | } 20 | return (stack.length !== 0); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /Parser/tests/anagraphCreator.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | Anagraph Object tests 3 | ************/ 4 | const anagraphCreator = require('../anagraphCreator') 5 | 6 | const sampleQuery = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | const anagraph = anagraphCreator(sampleQuery) 8 | 9 | describe('AnagraphCreator', () => { 10 | 11 | test('is a function', () => { 12 | expect(typeof anagraphCreator).toBe('function'); 13 | }) 14 | 15 | test('returns an object', () => { 16 | expect(typeof anagraphCreator(sampleQuery)).toBe('object'); 17 | }) 18 | 19 | test('has a property called analytics', () => { 20 | expect(anagraph).toHaveProperty('analytics') 21 | }) 22 | 23 | test('analytics property has a property called shallowResolvers', () => { 24 | expect(anagraph.analytics).toHaveProperty('shallowResolvers') 25 | }) 26 | 27 | test('analytics property has a property called specificResolvers', () => { 28 | expect(anagraph.analytics).toHaveProperty('specificResolvers') 29 | }) 30 | 31 | test('analytics property has a property called fields', () => { 32 | expect(anagraph.analytics).toHaveProperty('fields') 33 | }) 34 | 35 | test('analytics property has a property called maxNested', () => { 36 | expect(anagraph.analytics).toHaveProperty('maxNested') 37 | }) 38 | 39 | test('analytics property has a property called totalFields', () => { 40 | expect(anagraph.analytics).toHaveProperty('totalFields') 41 | }) 42 | 43 | test('the value of anagraph.analytics.shallowResolvers is an object', () => { 44 | expect(typeof anagraph.analytics.shallowResolvers).toBe('object') 45 | }) 46 | 47 | test('the value of anagraph.analytics.specificResolvers is an number', () => { 48 | expect(typeof anagraph.analytics.specificResolvers).toBe('object') 49 | }) 50 | 51 | test('the value of anagraph.analytics.fields is an object', () => { 52 | expect(typeof anagraph.analytics.fields).toBe('object') 53 | }) 54 | 55 | test('the value of anagraph.analytics.maxNested is an number', () => { 56 | expect(typeof anagraph.analytics.maxNested).toBe('number') 57 | }) 58 | 59 | test('the value of anagraph.analytics.totalFields is an number', () => { 60 | expect(typeof anagraph.analytics.totalFields).toBe('number') 61 | }) 62 | 63 | 64 | 65 | 66 | }) 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Parser/tests/queryParser.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | Query Parser tests 3 | ************/ 4 | const queryParser = require('../queryParser') 5 | const sampleQuery = '{ author { firstname }}' 6 | 7 | describe('queryParser', () => { 8 | test('is a function', () => { 9 | expect(typeof queryParser).toBe('function') 10 | }) 11 | test('returns an object', () => { 12 | expect(typeof queryParser(sampleQuery)).toBe('object'); 13 | }) 14 | 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /Parser/tests/stringProperties.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | stringProperties tests 3 | ************/ 4 | const stringProperties = require('../stringProperties') 5 | 6 | const sampleString = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | 8 | describe('stringProperties', () => { 9 | 10 | test('should throw an error on an empty string', () => { 11 | const emptyString = () => stringProperties(''); 12 | expect(emptyString).toThrow(TypeError); 13 | }); 14 | 15 | test('is an object', () => { 16 | expect(typeof stringProperties).toBe('object'); 17 | }) 18 | 19 | test('has a property called lengthOfString', () => { 20 | expect(stringProperties).toHaveProperty('lengthOfString') 21 | }) 22 | 23 | test('has a property called countResolvers', () => { 24 | expect(stringProperties).toHaveProperty('countResolvers') 25 | }) 26 | 27 | test('has a property called countDepth', () => { 28 | expect(stringProperties).toHaveProperty('countDepth') 29 | }) 30 | test('has a property called combinedResolvers', () => { 31 | expect(stringProperties).toHaveProperty('combinedResolvers') 32 | }) 33 | 34 | test('has a property called combinedFields', () => { 35 | expect(stringProperties).toHaveProperty('combinedFields') 36 | }) 37 | 38 | 39 | 40 | 41 | }) 42 | 43 | 44 | -------------------------------------------------------------------------------- /Parser/tests/stringValidator.test.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | stringProperties tests 3 | ************/ 4 | const stringValidator = require('../stringValidator') 5 | 6 | const sampleString = '“{ authors { firstname books { title authors { firstname books { title authors { firstname books { title } } } } } } }“' 7 | 8 | describe('stringValidator.balancedParentes', () => { 9 | test('is a function', () => { 10 | expect(typeof stringValidator.balancedParentes).toBe('function') 11 | }) 12 | test('should return a boolean', () => { 13 | expect(typeof stringValidator.balancedParentes(sampleString)).toBe('boolean') 14 | }) 15 | 16 | 17 | }) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is anagraphql? 2 | 3 | Anagraphql an express middleware package that can be used to analyze graphQL queries before they interact with a database. 4 | 5 | # Installation 6 | 7 | To install anagraphql, run the following command in the terminal: 8 | 9 | ``` 10 | npm i anagraphql 11 | ``` 12 | 13 | # Getting started 14 | 15 | To use anagraphql in your application, require it into the file where you're setting up your server. 16 | 17 | ``` 18 | const anagraphql = require('anagraphql'); 19 | ``` 20 | 21 | 22 | From there, add anagraphql to your app.use function, as you would with any other middleware function. To render the anagraphql interactive playground, set graphiql equal to true, as shown below: 23 | 24 | ``` 25 | app.use('/graphql', 26 | anagraphql({ 27 | schema, 28 | rules, 29 | anagraphqlPlayground: true 30 | }), 31 | graphqlHTTP({ 32 | schema, 33 | })); 34 | ``` 35 | # Anangraphql Interactive Playground 36 | 37 | More soon... 38 | 39 | -------------------------------------------------------------------------------- /anagraphqlEditor/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "root": true, 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "react/prop-types": 0 10 | } 11 | } -------------------------------------------------------------------------------- /anagraphqlEditor/buildRun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm run build 3 | cd ../graphqlTestServer/ 4 | npm i 5 | nodemon index.js -------------------------------------------------------------------------------- /anagraphqlEditor/dist/IMG_0672.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anagraphql/c1ebb52e98675f14118c7214756b86da0634a34b/anagraphqlEditor/dist/IMG_0672.jpg -------------------------------------------------------------------------------- /anagraphqlEditor/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | AnagraphQL 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /anagraphqlEditor/dist/stylesheet.css: -------------------------------------------------------------------------------- 1 | 2 | .history{ 3 | display:flex; 4 | } 5 | 6 | .rule-item { 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | .content { 12 | padding: 0 18px; 13 | background-color: #f1f1f1; 14 | max-height: 0; 15 | overflow: hidden; 16 | transition: max-height 0.2s ease-out; 17 | } 18 | .collapsible { 19 | background-color: #eee; 20 | color: #444; 21 | cursor: pointer; 22 | padding: 18px; 23 | width: 100%; 24 | border: none; 25 | text-align: left; 26 | outline: none; 27 | font-size: 15px; 28 | } 29 | .active, .collapsible:hover { 30 | background-color: #ccc; 31 | } 32 | 33 | 34 | #rules-container { 35 | display: grid; 36 | grid-template-columns: 33.3% 33.3% 33.3%; 37 | } 38 | 39 | .json-title { 40 | border-style: solid; 41 | border-width: medium; 42 | } 43 | .about { 44 | padding: 10px; 45 | } 46 | 47 | #rules { 48 | display:flex; 49 | flex-direction: column; 50 | } 51 | .current { 52 | border-width: 2px; 53 | border-bottom-width:2px; 54 | border-bottom-color:Black; 55 | border-bottom-style: solid; 56 | } 57 | .CodeMirror { 58 | border: 1px solid #eee; 59 | /* height: 100%; */ 60 | } 61 | 62 | body { 63 | margin: 0; 64 | padding: 0; 65 | height: 100%; 66 | color: #333; 67 | background-color: #FFF; 68 | font-family: 'Montserrat', sans-serif; 69 | 70 | } 71 | #policy-options{ 72 | display:grid; 73 | grid-template-columns: 20% 80%; 74 | } 75 | 76 | .radio-options{ 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | } 81 | 82 | #btn-container{ 83 | display:grid; 84 | grid-template-rows: 50% 50%; 85 | } 86 | 87 | #options{ 88 | display:grid; 89 | grid-template-columns: 33.3% 33.3% 33.3%; 90 | } 91 | 92 | 93 | 94 | 95 | #top{ 96 | display:grid; 97 | grid-template-columns: 50% 50%; 98 | } 99 | 100 | 101 | #title { 102 | text-align: center; 103 | } 104 | 105 | #nav { 106 | margin: 0; 107 | padding: 0; 108 | width: 200px; 109 | background-color: #FAFAFA; 110 | height: 100vh; 111 | border-right: 1px solid #bbb; 112 | /* overflow: auto; */ 113 | /* box-shadow: inset -4px 0px 15px #967d7d; */ 114 | 115 | } 116 | 117 | .timelineFlex { 118 | width: 100%; 119 | padding-left: 12px; 120 | display: flex; 121 | justify-content: space-between; 122 | 123 | 124 | } 125 | 126 | #nav img{ 127 | max-width: 100%; 128 | max-height:8rem; 129 | display: block; 130 | margin-left: auto; 131 | margin-right: auto; 132 | opacity: 0.6; 133 | } 134 | 135 | #nav a { 136 | display: block; 137 | color: #333; 138 | text-decoration: none; 139 | text-shadow: 2px; 140 | text-align: center; 141 | /* box-shadow: 0 5px #775d5d; */ 142 | transform: translateY(40px); 143 | font-size: 20px; 144 | padding-top: 15px; 145 | 146 | } 147 | 148 | #nav a:hover { 149 | text-decoration: underline; 150 | } 151 | 152 | h2 { 153 | background-color: #FAFAFA; 154 | color: #666; 155 | margin: 0px; 156 | padding: 0px; 157 | padding-left: 3vh; 158 | } 159 | 160 | h2.query { 161 | background-color: #FAFAFA; 162 | color: #222; 163 | margin: 0px; 164 | padding: 0px; 165 | padding-left: 3vh; 166 | } 167 | 168 | .historyTimeline { 169 | width: 90% !important; 170 | height: 50px !important; 171 | } 172 | #timeline{ direction: rtl} 173 | .policiesContainer{ 174 | padding: 10px; 175 | } 176 | option { 177 | size: 30px; 178 | color: green; 179 | } 180 | .marker { 181 | padding: 5px; 182 | width: 90%; 183 | display: flex; 184 | justify-content: space-between; 185 | } 186 | 187 | .historySlider { 188 | text-align: center; 189 | width: 90%; 190 | } 191 | 192 | 193 | .grid-container { 194 | height: 100%; 195 | width: 100%; 196 | display: grid; 197 | grid-template-columns: 1fr 1fr; 198 | grid-template-rows: 5fr 5fr 2fr; 199 | grid-template-areas: "GraphQL-Query Response" "Anagraph Policies" "Timeline Timeline"; 200 | } 201 | 202 | .Timeline { grid-area: Timeline; border-top: 1px solid #bbb;} 203 | .GraphQL-Query { grid-area: GraphQL-Query; border-bottom: 1px solid #bbb;} 204 | .Response { grid-area: Response; border-left: 1px solid #bbb; border-bottom: 1px solid #bbb; } 205 | .Policies { grid-area: Policies; border-left: 1px solid #bbb;} 206 | .Anagraph { grid-area: Anagraph; } -------------------------------------------------------------------------------- /anagraphqlEditor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anagraphqlEditor", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "dev": "NODE_ENV=development webpack-dev-server --open", 9 | "build": "NODE_ENV=production webpack", 10 | "start": "bash buildRun.sh" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "codemirror": "^5.48.2", 17 | "codemirror-graphql": "^0.8.3", 18 | "d3": "^5.9.7", 19 | "express": "^4.17.1", 20 | "graphql": "^14.4.2", 21 | "graphql-language-service-parser": "^1.2.2", 22 | "react": "^16.8.6", 23 | "react-animated-css": "^1.2.1", 24 | "react-codemirror2": "^6.0.0", 25 | "react-dom": "^16.8.6", 26 | "react-markdown": "^4.1.0", 27 | "react-redux": "^7.1.0", 28 | "react-router-dom": "^5.0.1", 29 | "react-syntax-highlighter": "^11.0.2", 30 | "redux": "^4.0.4", 31 | "redux-thunk": "^2.3.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.5.5", 35 | "@babel/preset-env": "^7.5.5", 36 | "@babel/preset-react": "^7.0.0", 37 | "babel-loader": "^8.0.6", 38 | "css-loader": "^3.1.0", 39 | "eslint": "^6.1.0", 40 | "eslint-config-airbnb": "^17.1.1", 41 | "eslint-config-airbnb-base": "^13.2.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "eslint-plugin-jsx-a11y": "^6.2.3", 44 | "eslint-plugin-react": "^7.14.3", 45 | "jest": "^24.8.0", 46 | "nodemon": "^1.19.1", 47 | "style-loader": "^0.23.1", 48 | "webpack": "^4.37.0", 49 | "webpack-cli": "^3.3.6", 50 | "webpack-dev-server": "^3.7.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import { buildClientSchema } from 'graphql'; 2 | import * as types from '../constants/actionTypes'; 3 | 4 | export const updateQueryHistory = query => ({ 5 | type: types.UPDATE_QUERY_HISTORY, 6 | payload: query, 7 | }); 8 | 9 | export const updateQuery = query => ({ 10 | type: types.UPDATE_QUERY, 11 | payload: query, 12 | }); 13 | 14 | export const updateCurrAnagraph = anagraph => ({ 15 | type: types.UPDATE_CURR_ANAGRAPH, 16 | payload: anagraph, 17 | }); 18 | 19 | export const getSchema = body => (dispatch) => { 20 | fetch('/graphql', { 21 | method: 'POST', 22 | headers: { 23 | Accept: 'application/json', 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: JSON.stringify(body), 27 | credentials: 'include', 28 | }) 29 | .then(response => response.json()) 30 | .then((schema) => { 31 | dispatch({ 32 | type: types.GET_SCHEMA, 33 | payload: { 34 | applicableRules: schema.applicableRules, 35 | schema: buildClientSchema(schema.data), 36 | }, 37 | }); 38 | }); 39 | }; 40 | 41 | export const updateCurrResponse = resp => ({ 42 | type: types.UPDATE_CURR_RESPONSE, 43 | payload: resp, 44 | }); 45 | 46 | export const getQueryResponse = ({ query, currRule }) => (dispatch) => { 47 | fetch('/graphql', { 48 | method: 'POST', 49 | headers: { 50 | Accept: 'application/json', 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify({ 54 | query, 55 | currRule, 56 | }), 57 | credentials: 'include', 58 | }) 59 | .then(response => response.json()) 60 | .then((data) => { 61 | dispatch({ type: types.GET_QUERY_RESPONSE, payload: data }); 62 | }) 63 | .catch(err => console.log(`error in fetch: ${err}`)); 64 | }; 65 | 66 | export const saveConfiguration = rules => ({ 67 | type: types.SAVE_CONFIGURATION, 68 | payload: rules, 69 | }); 70 | 71 | export const updateNestedQueries = num => ({ 72 | type: types.UPDATE_NESTED_QUERIES, 73 | payload: num, 74 | }); 75 | 76 | export const updateFields = num => ({ 77 | type: types.UPDATE_FIELDS, 78 | payload: num, 79 | }); 80 | 81 | export const updateResolvers = num => ({ 82 | type: types.UPDATE_RESOLVERS, 83 | payload: num, 84 | }); 85 | 86 | export const updateCurrRule = (rule) => { 87 | console.log(rule); 88 | return ({ 89 | type: types.UPDATE_CURR_RULE, 90 | payload: rule, 91 | }); 92 | }; 93 | 94 | export const updateShallowResolvers = obj => ({ 95 | type: types.UPDATE_SHALLOW_RESOLVERS, 96 | payload: obj, 97 | }); 98 | 99 | export const updateSpecificResolvers = obj => ({ 100 | type: types.UPDATE_SPECIFIC_RESOLVERS, 101 | payload: obj, 102 | }); 103 | 104 | export const deleteRule = (...params) => ({ 105 | type: types.DELETE_RULE, 106 | payload: params, 107 | }); 108 | 109 | export const updateClientRules = rule => ({ 110 | type: types.UPDATE_CLIENT_RULES, 111 | payload: rule, 112 | }); 113 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import SideBar from './SideBar'; 5 | 6 | 7 | const App = () => ( 8 |
9 | 10 | 11 | 12 |
13 | ); 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/ApplicableRules.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | updateFields, updateNestedQueries, updateResolvers, 5 | updateShallowResolvers, updateSpecificResolvers, 6 | } from '../actions/actions'; 7 | import Collapsible from './Collapsible'; 8 | 9 | const ApplicableRules = ({ availableRules, setRules }) => { 10 | const dispatch = useDispatch(); 11 | const { CLIENT_RULES } = useSelector(state => state.rules); 12 | const globalRules = []; 13 | if (availableRules.maxNested) { 14 | globalRules.push(( 15 |
16 |

Max Nested

17 | 30 |
31 | )); 32 | } 33 | if (availableRules.totalResolvers) { 34 | globalRules.push(( 35 |
36 |

Max Number of Resolvers

37 | 50 |
51 | )); 52 | } 53 | 54 | if (availableRules.totalFields) { 55 | globalRules.push(( 56 |
57 |

Max Number of Fields

58 | 71 |
72 | )); 73 | } 74 | 75 | 76 | const shallowResolvers = Object.keys(availableRules.shallowResolvers).map((key) => { 77 | const payload = {}; 78 | payload[key] = 4; 79 | return ( 80 |
81 | {' '} 82 |

{`Max ${key}`}

83 | 96 |
97 | ); 98 | }); 99 | 100 | const specificResolvers = Object.keys(availableRules.specificResolvers).map((key) => { 101 | const payload = {}; 102 | payload[key] = 4; 103 | return ( 104 |
105 | {' '} 106 |

{`Max ${key}`}

107 | 120 |
121 | ); 122 | }); 123 | 124 | return ( 125 |
126 | 127 | 128 | 129 |
130 | ); 131 | }; 132 | 133 | export default ApplicableRules; 134 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/CodeBlock.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SyntaxHighlighter from 'react-syntax-highlighter'; 3 | 4 | export default class CodeBlock extends React.PureComponent { 5 | render() { 6 | const { language, value } = this.props; 7 | 8 | return ( 9 | 10 | {value} 11 | 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/CodeContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { parse, print } from 'graphql'; 4 | import { 5 | getQueryResponse, updateQueryHistory, updateQuery, 6 | } from '../actions/actions'; 7 | import CodeEditor from './CodeEditor'; 8 | import JsonDisplay from './JsonDisplay'; 9 | import History from './History'; 10 | import Headline from './Headline'; 11 | // import MainContainer from "./containers/MainContainer.jsx"; 12 | const CodeContainer = () => { 13 | const response = useSelector(state => state.response.currResponse); 14 | const currAnagraph = useSelector(state => state.response.currAnagraph); 15 | const { currRule } = useSelector(state => state.rules); 16 | const { query } = useSelector(state => state.query); 17 | 18 | const [hasErrors, setErrors] = useState(true); 19 | const dispatch = useDispatch(); 20 | 21 | const prettifyQuery = () => dispatch(updateQuery(print(parse(query)))); 22 | 23 | const handleQuery = () => { 24 | if (!hasErrors) { 25 | prettifyQuery(); 26 | dispatch(getQueryResponse({ query, currRule })); 27 | dispatch(updateQueryHistory(print(parse(query)))); 28 | } 29 | }; 30 | 31 | return ( 32 |
33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default CodeContainer; 62 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/CodeEditor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Controlled as CodeMirror } from 'react-codemirror2'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { updateQuery } from '../actions/actions'; 5 | 6 | import 'codemirror/lib/codemirror'; 7 | import 'codemirror/lib/codemirror.css'; 8 | import 'codemirror/addon/hint/show-hint'; 9 | import 'codemirror/addon/hint/show-hint.css'; 10 | import 'codemirror/addon/comment/comment'; 11 | import 'codemirror/addon/edit/matchbrackets'; 12 | import 'codemirror/addon/edit/closebrackets'; 13 | import 'codemirror/addon/fold/foldgutter'; 14 | import 'codemirror/addon/fold/brace-fold'; 15 | import 'codemirror/addon/search/search'; 16 | import 'codemirror/addon/search/jump-to-line'; 17 | import 'codemirror/addon/dialog/dialog'; 18 | import 'codemirror/keymap/sublime'; 19 | import 'codemirror/addon/lint/lint'; 20 | import 'codemirror/addon/lint/lint.css'; 21 | import 'codemirror-graphql/hint'; 22 | import 'codemirror-graphql/lint'; 23 | import 'codemirror-graphql/info'; 24 | import 'codemirror-graphql/jump'; 25 | import 'codemirror-graphql/mode'; 26 | 27 | const CodeEditor = ({ hasErrors, setErrors, prettifyQuery }) => { 28 | const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/; 29 | 30 | const { query, schema } = useSelector(state => ({ 31 | query: state.query.query, 32 | schema: state.query.schema, 33 | })); 34 | const dispatch = useDispatch(); 35 | const options = { 36 | lineNumbers: true, 37 | tabSize: 2, 38 | keyMap: 'sublime', 39 | autoCloseBrackets: true, 40 | matchBrackets: true, 41 | showCursorWhenSelecting: true, 42 | foldGutter: { 43 | minFoldSize: 4, 44 | }, 45 | mode: 'graphql', 46 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 47 | lint: { 48 | schema, 49 | }, 50 | hintOptions: { 51 | schema, 52 | closeOnUnfocus: false, 53 | completeSingle: false, 54 | }, 55 | extraKeys: { 56 | 'Alt-P': () => { 57 | if (!hasErrors) prettifyQuery(); 58 | }, 59 | }, 60 | theme: 'default', 61 | }; 62 | 63 | 64 | return ( 65 |
66 | { 69 | if (AUTO_COMPLETE_AFTER_KEY.test(event.key)) { 70 | editor.execCommand('autocomplete'); 71 | } 72 | }} 73 | onBeforeChange={(editor, data, value) => dispatch(updateQuery(value))} 74 | onUpdate={editor => setErrors(editor.state.lint.marked.length !== 0)} 75 | options={options} 76 | /> 77 | 78 |
79 | ); 80 | }; 81 | 82 | export default CodeEditor; 83 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/Collapsible.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default ({ rules, text }) => { 4 | const [active, setActive] = useState(false); 5 | const toggle = () => setActive(!active); 6 | return ( 7 |
8 | 9 |
10 | {rules} 11 |
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/Graph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import * as d3 from 'd3'; 3 | 4 | const Graph = (props) => { 5 | useEffect(() => { 6 | const { data } = props; 7 | 8 | const adjlist = []; 9 | function neigh(a, b) { 10 | return a == b || adjlist[`${a}-${b}`]; 11 | } 12 | data.links.forEach((d) => { 13 | adjlist[`${d.source.index}-${d.target.index}`] = true; 14 | adjlist[`${d.target.index}-${d.source.index}`] = true; 15 | }); 16 | 17 | function focus(d) { 18 | const { index } = d3.select(d3.event.target).datum(); 19 | /* node.style('opacity', o => (neigh(index, o.index) ? 1 : 0.1)) */ 20 | // labelNode.attr('display', o => (neigh(index, o.node.index) ? 'block' : 'none')); 21 | link.style('stroke', o => (o.source.index == index ? 'blue' : o.target.index == index ? 'red' : 'black')); 22 | /* link.style('stroke', o => ( o.target.index == index ? 'red' : 'black')) */ 23 | link.attr('stroke-width', o => (o.source.index == index || o.target.index == index ? '3px' : '2px')); 24 | } 25 | 26 | function unfocus() { 27 | // labelNode.attr('display', 'block'); 28 | /* node.style('opacity', 1) */ 29 | link.style('stroke', 'black'); 30 | link.attr('stroke-width', '2px'); 31 | } 32 | 33 | // Create somewhere to put the force directed graph 34 | const svg = d3.select('svg'); 35 | const width = +svg.attr('width'); 36 | const height = +svg.attr('height'); 37 | 38 | const rectWidth = 140; 39 | const rectHeight = 120; 40 | const minDistance = Math.sqrt(rectWidth * rectWidth + rectHeight * rectHeight); 41 | 42 | // Set up the simulation and add forces 43 | const simulation = d3.forceSimulation() 44 | .nodes(data.nodes); 45 | 46 | const link_force = d3.forceLink(data.links) 47 | .id(d => d.id).distance(minDistance).strength(1); 48 | 49 | const charge_force = d3.forceManyBody() 50 | .strength(-1000); 51 | 52 | const center_force = d3.forceCenter(width / 2, height / 2); 53 | 54 | simulation 55 | .force('charge_force', charge_force) 56 | .force('center_force', center_force) 57 | .force('links', link_force) 58 | .force('y', d3.forceY(height / 2).strength(0.10)); 59 | 60 | 61 | // Add tick instructions: 62 | simulation.on('tick', tickActions); 63 | svg.append('svg:defs').append('svg:marker') 64 | .attr('id', 'arrow') 65 | .attr('viewBox', '0 0 12 12') 66 | .attr('refX', 10) 67 | .attr('refY', 6) 68 | .attr('markerWidth', 12) 69 | .attr('markerHeight', 12) 70 | .attr('orient', 'auto') 71 | .attr('fill', 'darkgreen') 72 | .append('svg:path') 73 | .attr('d', 'M2,2 L10,6 L2,10 L6,6 L2,2'); 74 | 75 | 76 | // Add encompassing group for the zoom 77 | const g = svg.append('g') 78 | .attr('class', 'everything'); 79 | 80 | const div = g.select('body').append('div') 81 | .attr('class', 'tooltip') 82 | .style('opacity', 0); 83 | 84 | // Draw lines for the links 85 | const link = g.append('g') 86 | .attr('class', 'links') 87 | .selectAll('line') 88 | .data(data.links) 89 | .enter() 90 | .append('path') 91 | .style('stroke', 'black') 92 | .style('fill', 'none') 93 | .attr('stroke-width', '2px') 94 | .attr('marker-start', 'url(#arrow)') 95 | .attr('marker-mid', 'url(#arrow)') 96 | .attr('marker-end', 'url(#arrow)'); 97 | /* .attr('marker-start', (d) => "url(#arrow)") 98 | .style( "stroke-width", 3 ); */ 99 | 100 | // Draw rects and texts for the nodes 101 | const nodes = g.append('g') 102 | .attr('class', 'nodes'); 103 | 104 | const node = nodes.selectAll('node') 105 | .data(data.nodes) 106 | .enter() 107 | .append('g'); 108 | 109 | node.on('mouseover', focus).on('mouseout', unfocus); 110 | 111 | 112 | const rect = node.append('rect') 113 | .attr('x', -rectWidth / 2) 114 | .attr('y', -rectHeight / 2) 115 | .attr('width', rectWidth) 116 | .attr('height', rectHeight) 117 | .attr('fill', rectColour); 118 | 119 | const textName = node.append('text') 120 | .text(d => d.name) 121 | .attr('y', function () { return this.previousSibling.y.baseVal.value; }) 122 | .style('text-anchor', 'middle') 123 | .attr('font-weight', 'bold'); 124 | 125 | /* const textCvr = node.append('text') 126 | .text(d => d.fields) 127 | .attr('y', 0) 128 | .style('text-anchor', 'middle'); */ 129 | const textCvr = node.each(function (d, i) { 130 | d3.select(this).selectAll('node') 131 | .data(d.fields) 132 | .enter() 133 | .append('text') 134 | .text(t => t) 135 | .attr('y', function (t, i) { return this.parentElement.firstChild.y.baseVal.value + (i + 1) * 16; }) 136 | .style('text-anchor', 'middle'); 137 | }); 138 | 139 | 140 | /* const textOwned = node.append('text') 141 | .text('%') 142 | .attr('y', 15) 143 | .style('text-anchor', 'middle'); */ 144 | 145 | node.attr('transform', d => `translate(${d.x},${d.y})`); 146 | 147 | // Add drag capabilities 148 | const drag_handler = d3.drag() 149 | .on('start', drag_start) 150 | .on('drag', drag_drag) 151 | .on('end', drag_end); 152 | 153 | drag_handler(node); 154 | 155 | // Add zoom capabilities 156 | const zoom_handler = d3.zoom() 157 | .on('zoom', zoom_actions); 158 | 159 | zoom_handler(svg); 160 | 161 | /** Functions * */ 162 | 163 | function rectColour(d) { 164 | if (d.person) { 165 | return 'blue'; 166 | } 167 | return 'pink'; 168 | } 169 | 170 | // Function to choose the line colour and thickness 171 | function linkColour(d) { 172 | return 'black'; 173 | } 174 | 175 | // Drag functions 176 | function drag_start(d) { 177 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 178 | d.fx = d.x; 179 | d.fy = d.y; 180 | } 181 | 182 | // Make sure you can't drag the rect outside the box 183 | function drag_drag(d) { 184 | d.fx = d3.event.x; 185 | d.fy = d3.event.y; 186 | } 187 | 188 | function drag_end(d) { 189 | if (!d3.event.active) simulation.alphaTarget(0); 190 | // d.fx = null; 191 | // d.fy = null; 192 | d.fx = d3.event.x; 193 | d.fy = d3.event.y; 194 | } 195 | 196 | // Zoom functions 197 | function zoom_actions() { 198 | g.attr('transform', d3.event.transform); 199 | } 200 | 201 | function tickActions() { 202 | // update node positions each tick of the simulation 203 | node.attr('transform', d => `translate(${d.x},${d.y})`); 204 | // update link positions 205 | link.attr('d', (d) => { 206 | const dx = d.target.x - d.source.x; 207 | const dy = d.target.y - d.source.y; 208 | const dr = Math.sqrt(dx * dx + dy * dy); 209 | return `M${ 210 | d.source.x},${ 211 | d.source.y}A${ 212 | dr},${dr} 0 0,1 ${ 213 | d.target.x},${ 214 | d.target.y}`; 215 | }); 216 | } 217 | }, []); 218 | 219 | return ; 220 | }; 221 | 222 | export default Graph; 223 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/Headline.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PolicySelector from './PolicySelector'; 3 | 4 | export default function Headline({ header }) { 5 | if (header === 'Policies') { 6 | return ( 7 |
8 |

{header}

9 | 10 |
11 | ); 12 | } 13 | return ( 14 |
15 |

{header}

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/History.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { Animated } from 'react-animated-css'; 4 | import { updateCurrResponse, updateCurrAnagraph, updateQuery } from '../actions/actions'; 5 | // import play from '../play.svg'; 6 | 7 | const History = ({ handleQuery, hasErrors }) => { 8 | const responseList = useSelector(state => state.response.history.response); 9 | const anagraphList = useSelector(state => state.response.history.anagraph); 10 | const queryList = useSelector(state => state.response.history.query); 11 | 12 | const dispatch = useDispatch(); 13 | const handleUpdate = (e) => { 14 | const ind = e.target.value; 15 | // document.querySelectorAll('.current').forEach(cv => cv.removeAttribute('class')); 16 | // e.currentTarget.parentElement.parentElement.setAttribute('class', 'current'); 17 | dispatch(updateCurrResponse(responseList[ind])); 18 | dispatch(updateCurrAnagraph(anagraphList[ind])); 19 | dispatch(updateQuery(queryList[ind])); 20 | }; 21 | let slider = 'Timeline will be built after a query...'; 22 | if (responseList.length > 0) { 23 | slider = ; 24 | } 25 | 26 | const markers = responseList.map((x, i) => ( 27 | 28 | 29 | 30 | )); 31 | 32 | return ( 33 |
34 |
35 |
36 |
37 | {markers} 38 |
39 | {slider} 40 |
41 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default History; 48 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/JsonDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Controlled as CodeMirror } from 'react-codemirror2'; 3 | import 'codemirror/addon/lint/lint.css'; 4 | import 'codemirror/addon/fold/foldgutter'; 5 | import 'codemirror/addon/dialog/dialog'; 6 | import 'codemirror/addon/search/search'; 7 | import 'codemirror/addon/search/searchcursor'; 8 | import 'codemirror/addon/search/jump-to-line'; 9 | import 'codemirror/keymap/sublime'; 10 | import 'codemirror/addon/fold/brace-fold'; 11 | import 'codemirror/addon/lint/lint'; 12 | import 'codemirror/addon/lint/json-lint'; 13 | import 'codemirror/mode/javascript/javascript'; 14 | import 'codemirror/lib/codemirror.css'; 15 | 16 | 17 | const JsonDisplay = ({ json }) => { 18 | const options = { 19 | lineNumbers: true, 20 | tabSize: 2, 21 | keyMap: 'sublime', 22 | autoCloseBrackets: true, 23 | matchBrackets: true, 24 | showCursorWhenSelecting: true, 25 | foldGutter: { 26 | minFoldSize: 4, 27 | }, 28 | mode: 'application/json', 29 | readOnly: true, 30 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 31 | }; 32 | return ( 33 | 37 | ); 38 | }; 39 | 40 | export default JsonDisplay; 41 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/PoliciesContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | // import RulesDisplay from './RulesDisplay'; 4 | import RulesConfiguration from './RulesConfiguration'; 5 | import JsonDisplay from './JsonDisplay'; 6 | import ApplicableRules from './ApplicableRules'; 7 | 8 | const PoliciesContainer = () => { 9 | const { CLIENT_RULES } = useSelector(state => state.rules); 10 | const { applicableRules } = useSelector(state => state.query); 11 | const [availableRules, setRules] = useState(applicableRules); 12 | 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default PoliciesContainer; 25 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/PolicySelector.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { updateCurrRule } from '../actions/actions'; 4 | 5 | const PolicySelector = () => { 6 | const [option, setOption] = useState('MERGE'); 7 | const { CLIENT_RULES, SERVER_RULES } = useSelector(state => state.rules); 8 | const dispatch = useDispatch(); 9 | const update = (str) => { 10 | setOption(str); 11 | switch (str) { 12 | case 'SERVER': 13 | dispatch(updateCurrRule(SERVER_RULES)); 14 | break; 15 | case 'CLIENT': 16 | dispatch(updateCurrRule(CLIENT_RULES)); 17 | break; 18 | case 'MERGE': 19 | dispatch(updateCurrRule({ 20 | ...SERVER_RULES, 21 | ...CLIENT_RULES, 22 | shallowResolvers: { 23 | ...SERVER_RULES.shallowResolvers, 24 | ...CLIENT_RULES.shallowResolvers, 25 | }, 26 | specificResolvers: { 27 | ...SERVER_RULES.specificResolvers, 28 | ...CLIENT_RULES.specificResolvers, 29 | }, 30 | })); 31 | break; 32 | default: 33 | dispatch(updateCurrRule(CLIENT_RULES)); 34 | } 35 | }; 36 | useEffect(() => update(option), [option]); 37 | return ( 38 |
39 |
40 | update(e.currentTarget.value)} 42 | type="radio" 43 | id="server" 44 | name="server" 45 | value="SERVER" 46 | checked={option === 'SERVER'} 47 | /> 48 | 49 |
50 |
51 | update(e.currentTarget.value)} 53 | type="radio" 54 | id="client" 55 | name="client" 56 | value="CLIENT" 57 | checked={option === 'CLIENT'} 58 | /> 59 | 60 |
61 |
62 | update(e.currentTarget.value)} 64 | type="radio" 65 | id="merge" 66 | name="merge" 67 | value="MERGE" 68 | checked={option === 'MERGE'} 69 | /> 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default PolicySelector; 77 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/RulesConfiguration.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { 4 | updateFields, updateNestedQueries, updateResolvers, 5 | updateShallowResolvers, updateSpecificResolvers, deleteRule, 6 | } from '../actions/actions'; 7 | 8 | const buildGlobal = (maxNested, totalResolvers, totalFields, dispatch, avail, setRules) => { 9 | const globalRules = []; 10 | if (maxNested) { 11 | globalRules.push( 12 |
13 | 25 |

Max number of nested queries allowed

26 | { dispatch(updateNestedQueries(Number(e.target.value))); }} id="numberOfNestedQueries" /> 27 | {maxNested} 28 |
, 29 | ); 30 | } 31 | 32 | if (totalResolvers) { 33 | globalRules.push( 34 |
35 | 46 |

Max number of resolvers allowed

47 | { dispatch(updateResolvers(Number(e.target.value))); }} id="numberOfResolvers" /> 48 | {totalResolvers} 49 |
, 50 | ); 51 | } 52 | 53 | if (totalFields) { 54 | globalRules.push( 55 |
56 | 68 | 69 |

Max number of fields allowed

70 | { dispatch(updateFields(Number(e.target.value))); }} id="numberOfFields" /> 71 | {totalFields} 72 |
, 73 | ); 74 | } 75 | 76 | return globalRules; 77 | }; 78 | 79 | const buildResolvers = (type, obj, action, dispatch, avail, setRules) => Object.keys(obj).map(cv => ( 80 |
81 | 93 |

94 | {cv} 95 | {' '} 96 | {`${type} resolver`} 97 |

98 | { 104 | const toReturn = {}; 105 | toReturn[cv] = Number(e.target.value); 106 | dispatch(action(toReturn)); 107 | }} 108 | /> 109 | {obj[cv] || 0} 110 |
111 | )); 112 | 113 | const RulesConfiguration = ({ setRules, availableRules }) => { 114 | const { 115 | maxNested, totalResolvers, totalFields, shallowResolvers, specificResolvers, 116 | } = useSelector(state => state.rules.CLIENT_RULES); 117 | const dispatch = useDispatch(); 118 | const globalRules = buildGlobal(maxNested, totalResolvers, totalFields, dispatch, availableRules, setRules); 119 | const shallowRules = buildResolvers('Shallow', shallowResolvers, updateShallowResolvers, dispatch, availableRules, setRules); 120 | const specificRules = buildResolvers('Specific', specificResolvers, updateSpecificResolvers, dispatch, availableRules, setRules); 121 | 122 | return ( 123 | 124 |
125 | {specificRules} 126 | {shallowRules} 127 | {globalRules} 128 |
129 | ); 130 | }; 131 | 132 | export default RulesConfiguration; 133 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/RulesDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { saveConfiguration, updateCurrRule } from '../actions/actions'; 4 | 5 | const RulesDisplay = () => { 6 | const rulesObject = useSelector(state => state.rules.currRule); 7 | const rulesList = useSelector(state => state.rules.rules); 8 | const dispatch = useDispatch(); 9 | 10 | const handleUpdate = (e) => { 11 | e.preventDefault(); 12 | dispatch(saveConfiguration({ name: e.target.firstChild.value, rules: rulesObject })); 13 | e.target.firstChild.value = ''; 14 | }; 15 | 16 | const onClickHandler = (index) => { 17 | dispatch(updateCurrRule(index)); 18 | }; 19 | 20 | const list = rulesList.map((rule, index) => ( 21 |
  • onClickHandler(index)} 24 | // onKeyUp={(e) => { 25 | // console.log(e.keyCode); 26 | // onClickHandler(index); 27 | // }} 28 | > 29 | {rule.name} 30 | {rule.rules.numberOfNestedQueries} 31 | {rule.rules.numberOfResolvers} 32 | {rule.rules.numberOfFields} 33 |
  • 34 | )); 35 | 36 | return ( 37 |
    38 |
    39 | 40 |
    41 | 44 |
    45 | ); 46 | }; 47 | 48 | export default RulesDisplay; 49 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Animated } from 'react-animated-css'; 3 | import { 4 | Route, 5 | Link, 6 | } from 'react-router-dom'; 7 | import { useSelector } from 'react-redux'; 8 | import ReactMarkdown from 'react-markdown'; 9 | import CodeBlock from './CodeBlock'; 10 | import CodeContainer from './CodeContainer'; 11 | import Visualizer from './Visualizer'; 12 | import PoliciesContainer from './PoliciesContainer'; 13 | 14 | const SideBar = () => { 15 | const routes = [ 16 | { 17 | path: '/graphql', 18 | exact: true, 19 | main: () => ( 20 | 21 | ), 22 | }, 23 | { 24 | path: '/graphql/about', 25 | exact: true, 26 | main: () => ( 27 | 34 | ), 35 | }, 36 | { 37 | path: '/graphql/policies', 38 | exact: true, 39 | main: () => { 40 | if (!useSelector(state => state.query.applicableRules)) return

    Loading...

    ; 41 | return ; 42 | }, 43 | }, 44 | { 45 | path: '/graphql/visualizer', 46 | exact: true, 47 | main: () => , 48 | }, 49 | ]; 50 | return ( 51 | 52 |
    53 | 72 |
    78 | {routes.map(route => ( 79 | 85 | ))} 86 |
    87 | 88 |
    89 |
    90 | ); 91 | }; 92 | 93 | export default SideBar; 94 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/components/Visualizer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import d3DataCreator from '../utility/d3DataCreator'; 4 | import Graph from './Graph'; 5 | 6 | const Visualizer = () => { 7 | const data = d3DataCreator(useSelector(state => state.query.schema)); 8 | return ( 9 |
    10 | 11 |
    12 | ); 13 | }; 14 | 15 | export default Visualizer; 16 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | // Describes actions needed for redux reducers 2 | // Basically CRUD actions for interacting with query state on the frontend 3 | // The functionality for these actionTypes are in the action file 4 | 5 | export const UPDATE_QUERY = 'UPDATE_QUERY'; 6 | export const GET_QUERY_RESPONSE = 'GET_QUERY_RESPONSE'; 7 | export const GET_SCHEMA = 'GET_SCHEMA'; 8 | export const UPDATE_CURR_RESPONSE = 'UPDATE_CURR_RESPONSE'; 9 | export const CREATE_ANAGRAPH = 'CREATE_ANAGRAPH'; 10 | export const UPDATE_CURR_ANAGRAPH = 'UPDATE_CURR_ANAGRAPH'; 11 | export const UPDATE_QUERY_HISTORY = 'UPDATE_QUERY_HISTORY'; 12 | export const SAVE_CONFIGURATION = 'SAVE_CONFIGURATION'; 13 | export const UPDATE_NESTED_QUERIES = 'UPDATE_NESTED_QUERIES'; 14 | export const UPDATE_FIELDS = 'UPDATE_FIELDS'; 15 | export const UPDATE_RESOLVERS = 'UPDATE_RESOLVERS'; 16 | export const UPDATE_CURR_RULE = 'UPDATE_CURR_RULE'; 17 | export const UPDATE_SHALLOW_RESOLVERS = 'UPDATE_SHALLOW_RESOLVERS'; 18 | export const UPDATE_SPECIFIC_RESOLVERS = 'UPDATE_SPECIFIC_RESOLVERS'; 19 | export const DELETE_RULE = 'DELETE_RULE'; 20 | export const UPDATE_CLIENT_RULES = 'UPDATE_CLIENT_RULES'; 21 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import App from './components/App'; 5 | import store from './store'; 6 | import '../../dist/stylesheet.css'; 7 | 8 | render( 9 | , document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/reducers/queryReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_QUERY, GET_SCHEMA, 3 | } from '../constants/actionTypes'; 4 | 5 | 6 | // Sets variables for inital query store 7 | const initialState = { 8 | query: '', 9 | schema: null, 10 | applicableRules: null, 11 | }; 12 | 13 | // The queryReducer function contains a seriers of conditionals that perform a specific action based on what action from our imported actions are passed in as an argument 14 | const queryReducer = (state = initialState, action) => { 15 | switch (action.type) { 16 | case UPDATE_QUERY: 17 | return { ...state, query: action.payload }; 18 | case GET_SCHEMA: 19 | return { 20 | ...state, 21 | schema: action.payload.schema, 22 | applicableRules: action.payload.applicableRules, 23 | }; 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default queryReducer; 30 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/reducers/responseReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_QUERY_RESPONSE, 3 | UPDATE_CURR_RESPONSE, 4 | CREATE_ANAGRAPH, 5 | UPDATE_CURR_ANAGRAPH, 6 | UPDATE_QUERY_HISTORY, 7 | } from '../constants/actionTypes'; 8 | 9 | 10 | // Sets variables for initial response store 11 | const initialState = { 12 | currResponse: null, 13 | currAnagraph: null, 14 | // responseList: [], 15 | // anagraphList: [], 16 | history: { response: [], anagraph: [], query: [] }, 17 | }; 18 | 19 | // The responseReducer function returns either a default response (state) or, if the GET_QUERY_RESPONSE action type is passed as an argument, returns the current response and response list. 20 | 21 | const responseReducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case GET_QUERY_RESPONSE: 24 | return { 25 | ...state, 26 | currResponse: { ...action.payload.data }, 27 | currAnagraph: action.payload.anagraph, 28 | // responseList: [...state.responseList, action.payload], 29 | history: { 30 | anagraph: [...state.history.anagraph, action.payload.anagraph], 31 | response: [...state.history.response, { ...action.payload.data }], 32 | query: [...state.history.query], 33 | }, 34 | }; 35 | case UPDATE_CURR_RESPONSE: 36 | return { 37 | ...state, 38 | currResponse: action.payload, 39 | }; 40 | case CREATE_ANAGRAPH: 41 | return { 42 | ...state, 43 | // anagraphList: [...state.anagraphList, action.payload], 44 | history: { 45 | anagraph: [...state.history.anagraph, action.payload], 46 | response: [...state.history.response], 47 | query: [...state.history.query], 48 | }, 49 | currAnagraph: action.payload, 50 | }; 51 | case UPDATE_CURR_ANAGRAPH: 52 | return { 53 | ...state, 54 | currAnagraph: action.payload, 55 | }; 56 | case UPDATE_QUERY_HISTORY: 57 | return { 58 | ...state, 59 | // anagraphList: [...state.anagraphList, action.payload], 60 | history: { 61 | anagraph: [...state.history.anagraph], 62 | response: [...state.history.response], 63 | query: [...state.history.query, action.payload], 64 | }, 65 | }; 66 | default: 67 | return state; 68 | } 69 | }; 70 | 71 | export default responseReducer; 72 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import queryReducer from './queryReducer'; 3 | import responseReducer from './responseReducer'; 4 | import rulesReducer from './rulesReducer'; 5 | 6 | //The combineReducers function (which is a method on the redux object) takes the evaluated results of both the queryReducer and the responseReducer and exports them an a single object. 7 | 8 | export default combineReducers({ 9 | query: queryReducer, 10 | response: responseReducer, 11 | rules: rulesReducer, 12 | }); 13 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/reducers/rulesReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_CURR_RULE, SAVE_CONFIGURATION, UPDATE_NESTED_QUERIES, 3 | UPDATE_RESOLVERS, UPDATE_FIELDS, UPDATE_SHALLOW_RESOLVERS, 4 | UPDATE_SPECIFIC_RESOLVERS, DELETE_RULE, UPDATE_CLIENT_RULES, 5 | } from '../constants/actionTypes'; 6 | 7 | const initialState = { 8 | currRule: queryRules || { 9 | shallowResolvers: {}, 10 | specificResolvers: {}, 11 | }, 12 | CLIENT_RULES: { 13 | shallowResolvers: {}, 14 | specificResolvers: {}, 15 | }, 16 | SERVER_RULES: queryRules || { 17 | shallowResolvers: {}, 18 | specificResolvers: {}, 19 | }, 20 | rules: [], 21 | }; 22 | 23 | 24 | const rulesReducer = (state = initialState, action) => { 25 | switch (action.type) { 26 | case SAVE_CONFIGURATION: 27 | return { 28 | ...state, 29 | rules: [...state.rules, action.payload], 30 | }; 31 | case UPDATE_CURR_RULE: 32 | return { 33 | ...state, 34 | currRule: action.payload, 35 | }; 36 | case UPDATE_NESTED_QUERIES: 37 | return { 38 | ...state, 39 | CLIENT_RULES: { 40 | ...state.CLIENT_RULES, 41 | maxNested: action.payload, 42 | }, 43 | }; 44 | case UPDATE_FIELDS: 45 | return { 46 | ...state, 47 | CLIENT_RULES: { 48 | ...state.CLIENT_RULES, 49 | totalFields: action.payload, 50 | }, 51 | }; 52 | case UPDATE_RESOLVERS: 53 | return { 54 | ...state, 55 | CLIENT_RULES: { 56 | ...state.CLIENT_RULES, 57 | totalResolvers: action.payload, 58 | }, 59 | }; 60 | case UPDATE_SHALLOW_RESOLVERS: 61 | return { 62 | ...state, 63 | CLIENT_RULES: { 64 | ...state.CLIENT_RULES, 65 | shallowResolvers: { ...state.CLIENT_RULES.shallowResolvers, ...action.payload }, 66 | }, 67 | }; 68 | case UPDATE_SPECIFIC_RESOLVERS: 69 | return { 70 | ...state, 71 | CLIENT_RULES: { 72 | ...state.CLIENT_RULES, 73 | specificResolvers: { ...state.CLIENT_RULES.specificResolvers, ...action.payload }, 74 | }, 75 | }; 76 | case DELETE_RULE: { 77 | if (action.payload.length === 2) { 78 | const temp = {}; 79 | temp[action.payload[0]] = { ...state.CLIENT_RULES[action.payload[0]] }; 80 | delete temp[action.payload[0]][action.payload[1]]; 81 | return { 82 | ...state, 83 | CLIENT_RULES: { 84 | ...state.CLIENT_RULES, 85 | ...temp, 86 | }, 87 | }; 88 | } 89 | 90 | const temp = { ...state, CLIENT_RULES: { ...state.CLIENT_RULES } }; 91 | delete temp.CLIENT_RULES[action.payload[0]]; 92 | return temp; 93 | } 94 | case UPDATE_CLIENT_RULES: 95 | return { 96 | ...state, 97 | CLIENT_RULES: action.payload, 98 | }; 99 | default: 100 | return state; 101 | } 102 | }; 103 | 104 | export default rulesReducer; 105 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { 4 | introspectionQuery, 5 | introspectionQueryName, 6 | } from './utility/introspectionQueries'; 7 | import reducers from './reducers/rootReducer'; 8 | import { getSchema } from './actions/actions'; 9 | 10 | // Creates Redux store 11 | 12 | const store = createStore(reducers, applyMiddleware(thunk)); 13 | store.dispatch(getSchema({ query: introspectionQuery, operationName: introspectionQueryName })); 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/utility/d3DataCreator.js: -------------------------------------------------------------------------------- 1 | export default (schema) => { 2 | const start = schema._queryType; 3 | const cache = {}; 4 | let i = 1; 5 | const nodes = [{ name: start.name, id: 1, fields: Object.keys(start._fields) }]; 6 | cache[start.name] = 1; 7 | const links = []; 8 | const helper = (x, prev) => { 9 | Object.values(x).forEach((obj) => { 10 | const nm = obj.type.name || obj.type.ofType.name; 11 | if (!(nm in cache)) { 12 | if (obj.type._fields || (obj.type.ofType && obj.type.ofType._fields)) { 13 | i += 1; 14 | const fields = obj.type._fields || obj.type.ofType._fields; 15 | const temp = { name: nm, id: i, fields: Object.keys(fields) }; 16 | nodes.push(temp); 17 | cache[nm] = i; 18 | links.push({ source: prev, target: i }); 19 | helper(fields, i); 20 | } 21 | } else { 22 | links.push({ source: i, target: cache[nm] }); 23 | } 24 | }); 25 | }; 26 | helper(start._fields, i); 27 | return { nodes, links }; 28 | }; 29 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/client/utility/introspectionQueries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2019 GraphQL Contributors. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { getOperationAST, parse, introspectionQuery } from 'graphql'; 9 | 10 | export { introspectionQuery } from 'graphql'; 11 | export const introspectionQueryName = getOperationAST(parse(introspectionQuery)) 12 | .name.value; 13 | 14 | // Some GraphQL services do not support subscriptions and fail an introspection 15 | // query which includes the `subscriptionType` field as the stock introspection 16 | // query does. This backup query removes that field. 17 | export const introspectionQuerySansSubscriptions = ` 18 | query ${introspectionQueryName} { 19 | __schema { 20 | queryType { name } 21 | mutationType { name } 22 | types { 23 | ...FullType 24 | } 25 | directives { 26 | name 27 | description 28 | locations 29 | args { 30 | ...InputValue 31 | } 32 | } 33 | } 34 | } 35 | 36 | fragment FullType on __Type { 37 | kind 38 | name 39 | description 40 | fields(includeDeprecated: true) { 41 | name 42 | description 43 | args { 44 | ...InputValue 45 | } 46 | type { 47 | ...TypeRef 48 | } 49 | isDeprecated 50 | deprecationReason 51 | } 52 | inputFields { 53 | ...InputValue 54 | } 55 | interfaces { 56 | ...TypeRef 57 | } 58 | enumValues(includeDeprecated: true) { 59 | name 60 | description 61 | isDeprecated 62 | deprecationReason 63 | } 64 | possibleTypes { 65 | ...TypeRef 66 | } 67 | } 68 | 69 | fragment InputValue on __InputValue { 70 | name 71 | description 72 | type { ...TypeRef } 73 | defaultValue 74 | } 75 | 76 | fragment TypeRef on __Type { 77 | kind 78 | name 79 | ofType { 80 | kind 81 | name 82 | ofType { 83 | kind 84 | name 85 | ofType { 86 | kind 87 | name 88 | ofType { 89 | kind 90 | name 91 | ofType { 92 | kind 93 | name 94 | ofType { 95 | kind 96 | name 97 | ofType { 98 | kind 99 | name 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | `; 109 | -------------------------------------------------------------------------------- /anagraphqlEditor/src/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | 6 | 7 | // Uses static file 8 | app.use('/dist', express.static(path.join(__dirname, '..', '..', 'dist'))); 9 | 10 | // Serves dist file and index.html? 11 | app.get('/', (req, res) => { 12 | res.sendFile(path.join(__dirname, '..', '..', 'dist', 'index.html')); 13 | }); 14 | 15 | app.listen(3000, () => console.log('listening on 3000')); 16 | -------------------------------------------------------------------------------- /anagraphqlEditor/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: process.env.NODE_ENV, 5 | entry: path.join(__dirname, 'src', 'client', 'index.jsx'), 6 | output: { 7 | publicPath: path.join(__dirname, '..'), 8 | path: path.join(__dirname, '..'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | }, 20 | }, 21 | { 22 | test: /\.s?css$/, 23 | loaders: ['style-loader', 'css-loader'], 24 | }, 25 | { 26 | test: /\.mjs$/, 27 | include: /node_modules/, 28 | type: 'javascript/auto', 29 | }, 30 | ], 31 | }, 32 | resolve: { 33 | extensions: ['.js', '.jsx', '.mjs', '.gql', '.graphql'], 34 | }, 35 | devServer: { 36 | contentBase: path.join(__dirname, 'dist'), 37 | publicPath: '/dist/', 38 | proxy: { 39 | '/api': 'http://localhost:3000', 40 | '/graphql': 'http://localhost:3000', 41 | }, 42 | 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /buildRun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ./anagraphqlEditor/ 3 | npm run build 4 | cd ../graphqlTestServer/ 5 | npm i 6 | nodemon index.js -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Wild Document! 9 | 10 | 11 | 12 |

    13 | Check it out! 14 |

    15 | 16 | 17 | -------------------------------------------------------------------------------- /graphqlTestServer/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "root": true 4 | } -------------------------------------------------------------------------------- /graphqlTestServer/async_db.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const user = require('os').userInfo().username; 4 | 5 | const host = 'localhost'; 6 | const database = 'anagraph_db'; 7 | const password = 'node_password'; 8 | const port = 5432; 9 | 10 | // postgres://YourUserName:YourPassword@YourHost:5432/YourDatabase 11 | const connString = `postgresql://${user}:${password}@${host}:${port}/${database}`; 12 | 13 | 14 | module.exports = connString; 15 | -------------------------------------------------------------------------------- /graphqlTestServer/buildRun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd ../anagraphqlEditor/ 3 | npm run build 4 | cd ../graphqlTestServer/ 5 | npm i 6 | nodemon index.js -------------------------------------------------------------------------------- /graphqlTestServer/config/asyncFakeData.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const pool = require('../database.js'); 3 | 4 | const genFakeData = async () => { 5 | const addPeople = async () => { 6 | const client = await pool.connect(); 7 | // Add People 8 | const promises = []; 9 | for (let i = 0; i < 1000; i += 1) { 10 | promises.push( 11 | client.query( 12 | ` 13 | INSERT INTO person 14 | (firstname, 15 | lastname) 16 | VALUES 17 | ($1, $2) 18 | `, 19 | [faker.name.firstName(), faker.name.lastName()], 20 | ), 21 | ); 22 | } 23 | return Promise.all(promises); 24 | }; 25 | const addBooks = async () => { 26 | const client = await pool.connect(); 27 | const promises = []; 28 | // Add People 29 | for (let i = 0; i < 1000; i += 1) { 30 | promises.push( 31 | client.query( 32 | ` 33 | INSERT INTO book 34 | (title, 35 | author_id) 36 | VALUES 37 | ($1, $2) 38 | `, 39 | [faker.commerce.productName(), Math.floor(Math.random() * 999) + 1], 40 | ), 41 | ); 42 | } 43 | return Promise.all(promises); 44 | }; 45 | const addReviews = async () => { 46 | const client = await pool.connect(); 47 | const promises = []; 48 | // Add People 49 | for (let i = 0; i < 600; i += 1) { 50 | promises.push( 51 | client.query( 52 | ` 53 | INSERT INTO review 54 | (book_id, 55 | person_id, 56 | review_body) 57 | VALUES 58 | ($1, $2, $3) 59 | `, 60 | [ 61 | Math.floor(Math.random() * 999) + 1, 62 | Math.floor(Math.random() * 999) + 1, 63 | faker.lorem.paragraph(), 64 | ], 65 | ), 66 | ); 67 | } 68 | return Promise.all(promises); 69 | }; 70 | const addUserReads = async () => { 71 | const client = await pool.connect(); 72 | // Add People 73 | const promises = []; 74 | for (let i = 0; i < 600; i += 1) { 75 | promises.push( 76 | client.query( 77 | ` 78 | INSERT INTO userRead 79 | (book_id, 80 | person_id) 81 | VALUES 82 | ($1, $2) 83 | `, 84 | [ 85 | Math.floor(Math.random() * 999) + 1, 86 | Math.floor(Math.random() * 999) + 1, 87 | ], 88 | ), 89 | ); 90 | } 91 | return Promise.all(promises); 92 | }; 93 | await addPeople(); 94 | await addBooks(); 95 | await addReviews(); 96 | await addUserReads(); 97 | console.log('worked!'); 98 | }; 99 | genFakeData(); 100 | -------------------------------------------------------------------------------- /graphqlTestServer/config/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Configuring database" 4 | 5 | export PGPASSWORD='node_password' 6 | 7 | # 8 | # Should grab your username 9 | # 10 | dropdb -U $USER anagraph_db 11 | createdb -U $USER anagraph_db; 12 | psql -U $USER anagraph_db < ./config/db.sql 13 | 14 | echo "Ana-graphql was configered"; 15 | 16 | node ./config/asyncFakeData.js 17 | 18 | echo "I am done I think!! :)"; -------------------------------------------------------------------------------- /graphqlTestServer/config/db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE person ( 2 | person_id SERIAL PRIMARY KEY, 3 | firstname VARCHAR, 4 | lastname VARCHAR 5 | ); 6 | 7 | CREATE TABLE book ( 8 | book_id SERIAL PRIMARY KEY, 9 | title VARCHAR(256), 10 | author_id INTEGER REFERENCES person(person_id) 11 | ); 12 | 13 | CREATE TABLE review ( 14 | review_id SERIAL PRIMARY KEY, 15 | book_id INTEGER REFERENCES book(book_id), 16 | person_id INTEGER REFERENCES person(person_id), 17 | review_body TEXT 18 | ); 19 | 20 | CREATE TABLE userRead( 21 | person_id INTEGER REFERENCES book(book_id), 22 | book_id INTEGER REFERENCES book(book_id), 23 | CONSTRAINT userread_personid_bookid_key UNIQUE (person_id, book_id) 24 | ) -------------------------------------------------------------------------------- /graphqlTestServer/config/fakeData.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const pool = require('../database.js'); 3 | 4 | 5 | const addPeopleTemplate = () => { 6 | pool.connect((err, client, done) => { 7 | if (err) throw err; 8 | // Add People 9 | for (let i = 0; i < 1000; i += 1) { 10 | client.query(` 11 | INSERT INTO person 12 | (firstname, 13 | lastname) 14 | VALUES 15 | ($1, $2) 16 | `, 17 | [faker.name.firstName(), faker.name.lastName()], 18 | (err, res) => { 19 | done(); 20 | if (err) { 21 | console.log(err.stack); 22 | } else { 23 | console.log(res.rows[0]); 24 | } 25 | }); 26 | } 27 | }); 28 | }; 29 | 30 | const addBooks = () => { 31 | pool.connect((err, client, done) => { 32 | if (err) throw err; 33 | // Add People 34 | for (let i = 0; i < 1000; i += 1) { 35 | client.query(` 36 | INSERT INTO book 37 | (title, 38 | author_id) 39 | VALUES 40 | ($1, $2) 41 | `, 42 | [faker.commerce.productName(), (Math.floor(Math.random() * 999) + 1)], 43 | (err, res) => { 44 | done(); 45 | if (err) { 46 | console.log(err.stack); 47 | } else { 48 | console.log(res.rows[0]); 49 | } 50 | }); 51 | } 52 | }); 53 | }; 54 | 55 | 56 | const addReviews = () => { 57 | pool.connect((err, client, done) => { 58 | if (err) throw err; 59 | // Add People 60 | for (let i = 0; i < 600; i += 1) { 61 | client.query(` 62 | INSERT INTO review 63 | (book_id, 64 | person_id, 65 | review_body) 66 | VALUES 67 | ($1, $2, $3) 68 | `, 69 | [(Math.floor(Math.random() * 999) + 1), (Math.floor(Math.random() * 999) + 1), faker.lorem.paragraph()], 70 | (err, res) => { 71 | done(); 72 | if (err) { 73 | console.log(err.stack); 74 | } else { 75 | console.log(res.rows[0]); 76 | } 77 | }); 78 | } 79 | }); 80 | }; 81 | 82 | const addUserReads = () => { 83 | pool.connect((err, client, done) => { 84 | if (err) throw err; 85 | // Add People 86 | for (let i = 0; i < 600; i += 1) { 87 | client.query(` 88 | INSERT INTO userRead 89 | (book_id, 90 | person_id) 91 | VALUES 92 | ($1, $2) 93 | `, 94 | [(Math.floor(Math.random() * 999) + 1), (Math.floor(Math.random() * 999) + 1)], 95 | (err, res) => { 96 | done(); 97 | if (err) { 98 | console.log(err.stack); 99 | } else { 100 | console.log(res.rows[0]); 101 | } 102 | }); 103 | } 104 | }); 105 | }; 106 | 107 | // addPeople(); 108 | // addBooks(); 109 | // addReviews(); 110 | // addUserReads(); 111 | -------------------------------------------------------------------------------- /graphqlTestServer/database.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const user = require('os').userInfo().username; 3 | 4 | 5 | module.exports = new Pool({ 6 | user, 7 | host: 'localhost', 8 | database: 'anagraph_db', 9 | password: 'node_password', 10 | port: 5432, 11 | }); 12 | -------------------------------------------------------------------------------- /graphqlTestServer/db_connectStr.js: -------------------------------------------------------------------------------- 1 | 2 | const user = require('os').userInfo().username; 3 | 4 | const host = 'localhost'; 5 | const database = 'anagraph_db'; 6 | const password = 'node_password'; 7 | const port = 5432; 8 | 9 | 10 | const connString = `postgresql://${user}:${password}@${host}:${port}/${database}`; 11 | 12 | 13 | module.exports = connString; 14 | -------------------------------------------------------------------------------- /graphqlTestServer/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const graphqlHTTP = require('express-graphql'); 4 | const anagraphql = require('../'); 5 | const schema = require('./schema'); 6 | 7 | const app = express(); 8 | const port = 3000; 9 | app.use(bodyParser.json()); 10 | 11 | const rules = { 12 | specificResolvers: { 13 | RootQueryType_authors: 3, 14 | }, 15 | shallowResolvers: { 16 | authors: 2, 17 | }, 18 | maxNested: 4, 19 | totalResolvers: 25, 20 | totalFields: 60, 21 | }; 22 | 23 | 24 | app.use('/graphql', 25 | (req, res, next) => next(), 26 | anagraphql({ schema, rules, graphiql: true }), 27 | graphqlHTTP({ 28 | schema, 29 | // graphiql: true, 30 | })); 31 | 32 | 33 | app.listen(port, () => (console.log(`something on port ${port}`))); 34 | -------------------------------------------------------------------------------- /graphqlTestServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphqltestserver", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "bash buildRun.sh", 7 | "config": "sh config/config.sh", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "anagraphql": "file:..", 14 | "body-parser": "^1.19.0", 15 | "express": "^4.17.1", 16 | "express-graphql": "^0.9.0", 17 | "faker": "^4.1.0", 18 | "graphql": "^14.4.2", 19 | "nodemon": "^1.19.1", 20 | "pg": "^7.11.0", 21 | "pg-promise": "^8.7.5" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^6.1.0", 25 | "eslint-config-airbnb": "^17.1.1", 26 | "eslint-plugin-import": "^2.18.2", 27 | "eslint-plugin-jsx-a11y": "^6.2.3", 28 | "eslint-plugin-react": "^7.14.3", 29 | "jest": "^24.8.0" 30 | }, 31 | "description": "" 32 | } 33 | -------------------------------------------------------------------------------- /graphqlTestServer/resolvers.js: -------------------------------------------------------------------------------- 1 | const pool = require('./database'); 2 | 3 | const resolvers = { 4 | 5 | Query: { 6 | getAllperson() { 7 | const sql = 'SELECT * FROM "person";'; 8 | return pool.query(sql) 9 | .then(res => res.rows) 10 | .catch(err => console.error('Error is: ', err)); 11 | }, 12 | getperson(parent, args, context, info) { 13 | let sql = 'SELECT * FROM "person"'; 14 | let whereClause = ' WHERE '; 15 | Object.keys(args).forEach((fieldName, i, arr) => { 16 | whereClause += `"${fieldName}" = '${args[fieldName]}'`; 17 | if (i !== arr.length - 1) whereClause += ' AND '; 18 | else whereClause += ';'; 19 | }); 20 | sql += whereClause; 21 | return pool.query(sql) 22 | .then(res => res.rows) 23 | .catch(err => console.error('Error is: ', err)); 24 | }, 25 | getAllbook() { 26 | const sql = 'SELECT * FROM "book";'; 27 | return pool.query(sql) 28 | .then(res => res.rows) 29 | .catch(err => console.error('Error is: ', err)); 30 | }, 31 | getbook(parent, args, context, info) { 32 | let sql = 'SELECT * FROM "book"'; 33 | let whereClause = ' WHERE '; 34 | Object.keys(args).forEach((fieldName, i, arr) => { 35 | whereClause += `"${fieldName}" = '${args[fieldName]}'`; 36 | if (i !== arr.length - 1) whereClause += ' AND '; 37 | else whereClause += ';'; 38 | }); 39 | sql += whereClause; 40 | return pool.query(sql) 41 | .then(res => res.rows) 42 | .catch(err => console.error('Error is: ', err)); 43 | }, 44 | getAllauthor() { 45 | const sql = 'SELECT * FROM "author";'; 46 | return pool.query(sql) 47 | .then(res => res.rows) 48 | .catch(err => console.error('Error is: ', err)); 49 | }, 50 | getauthor(parent, args, context, info) { 51 | let sql = 'SELECT * FROM "author"'; 52 | let whereClause = ' WHERE '; 53 | Object.keys(args).forEach((fieldName, i, arr) => { 54 | whereClause += `"${fieldName}" = '${args[fieldName]}'`; 55 | if (i !== arr.length - 1) whereClause += ' AND '; 56 | else whereClause += ';'; 57 | }); 58 | sql += whereClause; 59 | return pool.query(sql) 60 | .then(res => res.rows) 61 | .catch(err => console.error('Error is: ', err)); 62 | }, 63 | getAllreview() { 64 | const sql = 'SELECT * FROM "review";'; 65 | return pool.query(sql) 66 | .then(res => res.rows) 67 | .catch(err => console.error('Error is: ', err)); 68 | }, 69 | getreview(parent, args, context, info) { 70 | let sql = 'SELECT * FROM "review"'; 71 | let whereClause = ' WHERE '; 72 | Object.keys(args).forEach((fieldName, i, arr) => { 73 | whereClause += `"${fieldName}" = '${args[fieldName]}'`; 74 | if (i !== arr.length - 1) whereClause += ' AND '; 75 | else whereClause += ';'; 76 | }); 77 | sql += whereClause; 78 | return pool.query(sql) 79 | .then(res => res.rows) 80 | .catch(err => console.error('Error is: ', err)); 81 | }, 82 | }, 83 | 84 | person: { 85 | person_id: (parent, args, context, info) => parent.person_id, 86 | firstname: (parent, args, context, info) => parent.firstname, 87 | lastname: (parent, args, context, info) => parent.lastname, 88 | }, 89 | 90 | book: { 91 | book_id: (parent, args, context, info) => parent.book_id, 92 | title: (parent, args, context, info) => parent.title, 93 | person: (parent, args, context, info) => { 94 | const sql = `SELECT * FROM "person" WHERE "person_id" = '${parent.author_id}';`; 95 | return pool.query(sql) 96 | .then(res => res.rows[0]) 97 | .catch(err => console.error('Error is: ', err)); 98 | }, 99 | }, 100 | 101 | author: { 102 | person: (parent, args, context, info) => { 103 | const sql = `SELECT * FROM "person" WHERE "person_id" = '${parent.person_id}';`; 104 | return pool.query(sql) 105 | .then(res => res.rows[0]) 106 | .catch(err => console.error('Error is: ', err)); 107 | }, 108 | book: (parent, args, context, info) => { 109 | const sql = `SELECT * FROM "book" WHERE "book_id" = '${parent.book_id}';`; 110 | return pool.query(sql) 111 | .then(res => res.rows[0]) 112 | .catch(err => console.error('Error is: ', err)); 113 | }, 114 | title: (parent, args, context, info) => parent.title, 115 | }, 116 | 117 | review: { 118 | review_id: (parent, args, context, info) => parent.review_id, 119 | book: (parent, args, context, info) => { 120 | const sql = `SELECT * FROM "book" WHERE "book_id" = '${parent.book_id}';`; 121 | return pool.query(sql) 122 | .then(res => res.rows[0]) 123 | .catch(err => console.error('Error is: ', err)); 124 | }, 125 | person: (parent, args, context, info) => { 126 | const sql = `SELECT * FROM "person" WHERE "person_id" = '${parent.person_id}';`; 127 | return pool.query(sql) 128 | .then(res => res.rows[0]) 129 | .catch(err => console.error('Error is: ', err)); 130 | }, 131 | review_body: (parent, args, context, info) => parent.review_body, 132 | }, 133 | 134 | }; 135 | 136 | module.exports = resolvers; 137 | -------------------------------------------------------------------------------- /graphqlTestServer/schema.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLID, 4 | GraphQLString, 5 | GraphQLInt, 6 | GraphQLNonNull, 7 | GraphQLList, 8 | GraphQLSchema, 9 | } = require('graphql'); 10 | const pgp = require('pg-promise')(); 11 | const connectionString = require('./async_db'); 12 | 13 | const db = {}; 14 | db.conn = pgp(connectionString); 15 | 16 | const BookType = new GraphQLObjectType({ 17 | name: 'book', 18 | fields: () => ({ 19 | book_id: { type: GraphQLID }, 20 | title: { type: GraphQLString }, 21 | author_id: { type: GraphQLID }, 22 | authors: { 23 | type: new GraphQLList(AuthorType), 24 | resolve(parentValue, args) { 25 | const query = `SELECT * FROM "person" WHERE person_id=${parentValue.author_id}`; 26 | return db.conn.many(query) 27 | .then(data => data); 28 | }, 29 | }, 30 | }), 31 | }); 32 | 33 | const AuthorType = new GraphQLObjectType({ 34 | name: 'author', 35 | fields: () => ({ 36 | person_id: { type: GraphQLID }, 37 | firstname: { type: GraphQLString }, 38 | lastname: { type: GraphQLString }, 39 | books: { 40 | type: new GraphQLList(BookType), 41 | resolve(parentValue, args) { 42 | const query = `SELECT * FROM "book" WHERE author_id=${parentValue.person_id}`; 43 | return db.conn.many(query) 44 | .then(data => data); 45 | }, 46 | }, 47 | }), 48 | }); 49 | 50 | const PersonType = new GraphQLObjectType({ 51 | name: 'person', 52 | fields: () => ({ 53 | person_id: { type: GraphQLID }, 54 | firstname: { type: GraphQLString }, 55 | lastname: { type: GraphQLString }, 56 | }), 57 | }); 58 | 59 | const ReviewType = new GraphQLObjectType({ 60 | name: 'review', 61 | fields: () => ({ 62 | review_id: { type: GraphQLID }, 63 | book_id: { type: GraphQLID }, 64 | person_id: { type: GraphQLID }, 65 | review_body: { type: GraphQLString }, 66 | book: { 67 | type: BookType, 68 | resolve(parentValue, args) { 69 | const query = `SELECT * FROM "book" WHERE book_id=${parentValue.book_id}`; 70 | return db.conn.one(query) 71 | .then(data => data); 72 | }, 73 | }, 74 | reviewAuthor: { 75 | type: PersonType, 76 | resolve(parentValue, args) { 77 | const query = `SELECT * FROM "person" WHERE person_id=${parentValue.person_id}`; 78 | return db.conn.one(query) 79 | .then(data => data); 80 | }, 81 | }, 82 | }), 83 | }); 84 | 85 | 86 | const RootQuery = new GraphQLObjectType({ 87 | name: 'RootQueryType', 88 | fields: { 89 | person: { 90 | type: PersonType, 91 | args: { id: { type: GraphQLID } }, 92 | resolve(parentValue, args) { 93 | const query = `SELECT * FROM "person" WHERE person_id=${args.id}`; 94 | return db.conn.one(query) 95 | .then(data => data); 96 | }, 97 | }, 98 | authors: { 99 | type: new GraphQLList(AuthorType), 100 | args: { limit: { type: GraphQLInt } }, 101 | resolve(parentValue, args) { 102 | const query = `SELECT DISTINCT person.person_id, person.firstname, person.lastname 103 | FROM book join person 104 | ON book.author_id = person.person_id LIMIT ${args.limit};`; 105 | return db.conn.many(query) 106 | .then(data => data); 107 | }, 108 | }, 109 | reviews: { 110 | type: new GraphQLList(ReviewType), 111 | args: { limit: { type: new GraphQLNonNull(GraphQLInt) } }, 112 | resolve(parentValue, args) { 113 | const query = `SELECT * FROM review LIMIT ${args.limit};`; 114 | return db.conn.many(query) 115 | .then(data => data); 116 | }, 117 | }, 118 | }, 119 | }); 120 | 121 | module.exports = new GraphQLSchema({ 122 | query: RootQuery, 123 | }); 124 | 125 | 126 | // Queries to be performed: 127 | 128 | // Select all Authors of books: 129 | const selectAuthors = ` 130 | SELECT DISTINCT book.author_id, person.firstname 131 | FROM book join person 132 | ON book.author_id = person.person_id; 133 | `; 134 | 135 | const selectReviewers = ` 136 | `; 137 | const selectReaders = ` 138 | `; 139 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const renderGraphiql = require('./renderGraphiql'); 3 | const anagraphCreator = require('./Parser/anagraphCreator'); 4 | const schemaParser = require('./Parser/schemaParser'); 5 | const ruleValidator = require('./Parser/ruleValidator'); 6 | const queryValidator = require('./Parser/queryValidator'); 7 | 8 | const anagraphql = options => ((req, res, next) => { 9 | if (req.method !== 'GET' && req.method !== 'POST') { 10 | res.setHeader('Allow', 'GET, POST'); 11 | res.status(405).send('GraphQL only supports GET and POST requests.'); 12 | res.end(); 13 | } 14 | 15 | const { schema, graphiql, rules } = options; 16 | if (!schema) throw new Error('GraphQL middleware options must contain a schema.'); 17 | const applicableRules = schemaParser(schema); 18 | 19 | if (req.body.operationName !== undefined && graphiql) { 20 | const oldSend = res.send; 21 | res.send = function (...data) { 22 | // arguments[0] (or `data`) contains the response body 23 | res.send = oldSend; 24 | data[0] = JSON.stringify({ ...JSON.parse(data[0]), applicableRules }); 25 | oldSend.apply(res, data); 26 | }; 27 | return next(); 28 | } 29 | 30 | 31 | if (graphiql && req.method === 'GET') { 32 | if (!graphiql) return next(); 33 | if (rules === undefined) res.send(renderGraphiql()); 34 | if (rules) res.send(renderGraphiql(rules)); 35 | return res.end(); 36 | } 37 | 38 | if (req.body.query) { 39 | const RULES_TO_CHECK = req.body.currRule || rules 40 | console.log(`RULES IN BACKEND: ${JSON.stringify(RULES_TO_CHECK)}`); 41 | const anagraph = anagraphCreator(req.body.query); 42 | 43 | if (RULES_TO_CHECK) { 44 | const validateRules = ruleValidator(applicableRules, RULES_TO_CHECK); 45 | if (validateRules.error) { 46 | res.status(422).send({ error: validateRules.error }); 47 | return res.end(); 48 | } 49 | const evaluation = queryValidator(anagraph, RULES_TO_CHECK); 50 | if (evaluation.error) { 51 | res.status(422).send({ error: evaluation.error }); 52 | return res.end(); 53 | } 54 | } 55 | 56 | 57 | if (graphiql) { 58 | const oldSend = res.send; 59 | res.send = function(...data){ 60 | // arguments[0] (or `data`) contains the response body 61 | res.send = oldSend 62 | data[0] = JSON.stringify({ ...JSON.parse(data[0]), anagraph }); 63 | oldSend.apply(res, data); 64 | } 65 | return next(); 66 | } 67 | 68 | return next(); 69 | 70 | } 71 | }); 72 | 73 | module.exports = anagraphql; 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anagraphql", 3 | "version": "1.0.1", 4 | "description": "Express middleware used to analyze graphQL queries before interacting with the database.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "bash buildRun.sh" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/oslabs-beta/anagraphql.git" 13 | }, 14 | "keywords": [ 15 | "graphql", 16 | "analyze", 17 | "policies", 18 | "monitoring" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/oslabs-beta/anagraphql/issues" 24 | }, 25 | "homepage": "https://github.com/oslabs-beta/anagraphql#readme", 26 | "devDependencies": { 27 | "jest": "^24.8.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /renderGraphiql.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | module.exports = (rules) => { 5 | const content = fs.readFileSync(path.join(__dirname, 'bundle.js'), 'utf-8'); 6 | const readMe = fs.readFileSync(path.join(__dirname, 'README.md'), 'utf-8').replace(/`/g, '\\`'); 7 | 8 | return ` 9 | 10 | 11 | 12 | 13 | 14 | AnagraphQL 15 | 16 | 17 | 22 | 23 | 24 |
    25 | 26 | 27 | 28 | `; 29 | }; 30 | --------------------------------------------------------------------------------