├── .eslintrc.js ├── .gitignore ├── Example ├── index.js └── manifest.js ├── LICENSE ├── README.md ├── __tests__ ├── queryGenTests.js └── routerCreationTest.js ├── index.js ├── package-lock.json ├── package.json ├── queryMap.js └── routerCreation.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es2021: true, 6 | }, 7 | extends: ['airbnb-base'], 8 | parserOptions: { 9 | ecmaVersion: 12, 10 | }, 11 | rules: { 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # DS_Store file 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /Example/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable no-undef */ 3 | /* eslint-disable no-console */ 4 | /* eslint-disable max-len */ 5 | /* eslint-disable import/no-unresolved */ 6 | /* eslint-disable import/no-extraneous-dependencies */ 7 | 8 | /* Instructions */ 9 | // Copy the this index.js file and the manifest.js file from the Example folder into a new directory on your machine 10 | // Within that directory, run: npm i express graphql @graphql-tools/schema monarq 11 | // Then run: node index 12 | // Follow the instructions starting on line 117 to run example REST requests 13 | 14 | /* This first section is set up for a basic GraphQL server - nothing specific to monarq just yet */ 15 | const express = require('express'); 16 | const { graphql } = require('graphql'); 17 | // eslint-disable-next-line import/no-unresolved 18 | const { makeExecutableSchema } = require('@graphql-tools/schema'); 19 | const { routerCreation, queryMap } = require('monarq'); 20 | const { manifest } = require('./manifest'); 21 | 22 | const app = express(); 23 | app.use(express.json()); 24 | 25 | // Example GraphQL Schema 26 | const typeDefs = ` 27 | type Query { 28 | getBook(id: ID!): Book 29 | } 30 | 31 | type Mutation { 32 | createAuthor(name: String!): Author! 33 | createBook(name: String!, author: ID!): Book! 34 | } 35 | 36 | type Book { 37 | id: ID! 38 | name: String! 39 | author: Author! 40 | } 41 | 42 | type Author { 43 | id: ID! 44 | name: String! 45 | } 46 | `; 47 | 48 | const resolvers = { 49 | Query: { 50 | getBook: (_, { id }) => ({ 51 | id, 52 | name: 'A Good Book', 53 | author: { 54 | id: 45, 55 | name: 'Alex M.', 56 | }, 57 | }), 58 | }, 59 | Mutation: { 60 | createBook: (_, { name, author }) => ({ 61 | id: 101, 62 | name, 63 | author: { 64 | id: 45, 65 | name: author, 66 | }, 67 | }), 68 | createAuthor: (_, { name }) => ({ 69 | id: 50, 70 | name, 71 | }), 72 | }, 73 | }; 74 | 75 | // A schema of type GraphQLSchema object is required to use monarq (either buildSchema from the graphql module or makeExecutableSchema from the @graphql-tools/schema module can be used to generate the schema object) 76 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 77 | 78 | /* This next section contains the code that you will need to implement from monarq in order to handle REST requests */ 79 | 80 | // STEP 1 81 | // Invoke the queryMap function installing monarq 82 | const createdQuery = queryMap(manifest, schema); 83 | // The console log below purely informational, showing the object that is returned from queryMap 84 | console.log('queryMap Object', createdQuery); 85 | 86 | // STEP 2 87 | // You will need to define this executeFunction, which has four parameters, and will be used by the monarq middleware to execute the GraphQL query 88 | // executeFn is a wrapper that should be placed around whatever method you are currently using to execute GraphQL queries. In this example, the native graphql method from graphql.js is used. 89 | async function executeFn({ 90 | query, variables, schema, context, 91 | }) { 92 | const data = await graphql(schema, query, null, context, variables); 93 | 94 | return data || errors; 95 | } 96 | 97 | // STEP 3 98 | // Invoke the routerCreation function to create a new express router that will handle all REST requests 99 | // For this example, context has been declared as an empty object because it is a required argument for routerCreation. However, your context argument will likely already exist and be populated with data (e.g., related to the database) 100 | const context = {}; 101 | 102 | const apiRouter = routerCreation(manifest, createdQuery, { 103 | schema, 104 | context, 105 | executeFn, 106 | }); 107 | 108 | // STEP 4 109 | // Implement the apiRouter in your server so that all REST requests are directed here 110 | app.use('/api', apiRouter); 111 | 112 | app.listen(4000); 113 | 114 | /* To see monarq in action, try each of the following HTTP requests using curl */ 115 | // curl -X GET http://localhost:4000/api/book/100 -H 'content-type: application/json' // expected response-> {"data":{"getBook":{"id":"100","name":"A Good Book","author":{"id":"45","name":"Alex M."}}}} 116 | // curl -X POST http://localhost:4000/api/authors -H 'content-type: application/json' -d '{ "name": "Taylor A." }' // expected response-> {"data":{"createAuthor":{"id":"50","name":"Taylor A."}}} 117 | // curl -X POST http://localhost:4000/api/books -H 'content-type: application/json' -d '{ "name": "Cool Book", "author": "45" }' // expected response-> {"data":{"createBook":{"id":"101","name":"Cool Book","author":{"id":"45","name":"45"}}}} 118 | 119 | /* The requests below demonstrate error messaging for when the user fails to provide necessary arguments with the query. It is recommended to clearly explain all necessary arguments for each REST endpoint in documentation that you provide REST users */ 120 | // curl -X POST http://localhost:4000/api/authors -H 'content-type: application/json' // expected response-> "Issue Executing Request: Variable \"$name\" of required type \"String!\" was not provided." 121 | // curl -X POST http://localhost:4000/api/books -H 'content-type: application/json' -d '{ "name": "Cool Book" }' // expected response-> "Issue Executing Request: Variable \"$author\" of required type \"ID!\" was not provided." 122 | -------------------------------------------------------------------------------- /Example/manifest.js: -------------------------------------------------------------------------------- 1 | const manifest = { 2 | endpoints: { 3 | '/book/:id': { 4 | get: { 5 | operation: 'getBook', 6 | }, 7 | }, 8 | '/books': { 9 | post: { 10 | operation: 'createBook', 11 | }, 12 | }, 13 | '/authors': { 14 | post: { 15 | operation: 'createAuthor', 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | module.exports = { manifest }; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MONARQ 2 | 3 | **MONARQ** makes it easy to accept REST requests with an existing Express/GraphQL server. Expand your application's API compatability with just a few lines of code. 4 | 5 | 6 | ## Contents 7 | [_Installation_](#installation) 8 | [_How It Works_](#how-it-works) 9 | [_Required User Inputs_](#required-user-inputs) 10 | [_Getting Started_](#getting-started) 11 | [_Keep in Mind_](#keep-in-mind) 12 | 13 | 14 | 15 | # Installation 16 | 17 | Install **MONARQ** through the command line 18 | 19 | ```bash 20 | npm install monarq 21 | ``` 22 | 23 | Now the two core functions of the package can be imported into your main Express/GraphQL server. 24 | 25 | ```javascript 26 | import { queryMap, routerCreation } from 'monarq'; 27 | ``` 28 | 29 | # How It Works 30 | 31 | There are two functions that need to be invoked within your main Express/GraphQL server file. 32 | 33 | ### Step 1: 34 | `queryMap` generates an object containing GraphQL queries/mutations and arguments for each operation specified in the [Manifest Object](#required-user-inputs). This 'map' of queries will be used to match REST requests to corresponding GQL operations. `queryMap` takes up to three arguments: 35 | 36 | - [Manifest Object](#required-user-inputs) 37 | - Schema of type GQLSchema object 38 | - (_Optional_) Array containing any custom scalar types declared in your schema 39 | 40 | Invoke this function in your main Express/GraphQL server file and assign the result to a constant. 41 | 42 | _Examples_ 43 | 44 | If no custom scalar types are used: 45 | ```javascript 46 | const createdQuery = queryMap(manifest, schema); 47 | ``` 48 | OR 49 | 50 | If a custom scalar type of 'Date' is defined in the schema: 51 | ```javascript 52 | const createdQuery = queryMap(manifest, schema, ['Date']); 53 | ``` 54 | 55 | ### Step 2: 56 | `routerCreation` returns an express.Router instance containing route handlers for each of the API Paths defined within the [Manifest Object](#required-user-inputs). `routerCreation` takes three arguments: 57 | 58 | - [Manifest Object](#required-user-inputs) 59 | - `createdQuery` (the saved value from invoking the queryMap function) 60 | - An Object with three keys: schema, context, and your created [`executeFn`](#required-user-inputs) 61 | 62 | _Example_ 63 | 64 | ```javascript 65 | const routes = routerCreation(manifest, createdQuery, { 66 | schema, 67 | context, 68 | executeFn, 69 | }); 70 | ``` 71 | 72 | ### Step 3: 73 | Implement the newly created router with `app.use` in your server file. All REST requests should be directed to the same top-level path (e.g., '/rest') so that the `routes` middleware will be applied. 74 | 75 | _Example_ 76 | 77 | ```javascript 78 | app.use('/rest', routes); 79 | ``` 80 | 81 | That's it! Your application is now ready to accept both GraphQL and REST requests. 82 | 83 | # Required User Inputs 84 | 85 | Before implementing `queryMap` and `routerCreation`, you will have to define the REST endpoints that you want to expose. Each REST endpoint will be mapped to a GraphQL operation, thus allowing the client to send REST requests and you to handle those requests with your GraphQL server. **MONARQ** gives you the flexibility to define as many or as few REST endpoints as you want to expose. 86 | 87 | Create a manifest object in a file and import into your server file. You can also visit **MONARQ**'s [website](http://monarq.io/) to create the manifest object using a simple visual interface. 88 | 89 | ### 1) DEFINE MANIFEST OBJECT 90 | 91 | The Manifest Object must be in a specific format as shown in the following example: 92 | 93 | ```javascript 94 | const manifest = { 95 | endpoints: { 96 | '/book/:id': { 97 | get: { 98 | operation: 'getBook', 99 | }, 100 | }, 101 | '/books': { 102 | post: { 103 | operation: 'createBook', 104 | }, 105 | }, 106 | '/authors': { 107 | post: { 108 | operation: 'createAuthor', 109 | }, 110 | }, 111 | }, 112 | }; 113 | ``` 114 | 115 | This Manifest Object is a required input into both `queryMap` and `routerCreation`. Each manifest object will contain: 116 | 117 | - A key called **endpoints**, with value object 118 | - The **endpoints** object will have all the **REST API Paths** you want to expose as the keys with the value of an object 119 | - Remember to have clients pre-pend these paths with whatever top-level path was defined in your route handler when making requests (e.g., '/rest/books') 120 | - Inside each **REST API Path** object, the keys will have one of these five labels: **GET, POST, PUT, PATCH, DELETE**. These methods correspond with the HTTP method that you want the client to send the request as. 121 | - Inside each **HTTP method** object, a required key `operation` will have the value of a string that corresponds to the GraphQL Query or Mutation operation name that should be invoked when the REST endpoint is requested. Note: the operation must be present within the GraphQL schema. 122 | - _Optional_: if the `operation` requires arguments and default parameters have been defined, you must specify these within an object at key **defaultParams**, also within the **HTTP method** object 123 | 124 | _Excerpt of an example schema corresponding to the the manifest object above:_ 125 | 126 | ```graphql 127 | type Query { 128 | books(pageSize: Int!, page: Int!): Books! 129 | book(id: ID!): Book 130 | } 131 | 132 | type Mutation { 133 | updateBook(id: ID!, book: BookUpdateInput!): Book! 134 | } 135 | ``` 136 | 137 | 138 | 139 | ### 2) DEFINE EXECUTE FUNCTION 140 | 141 | One of the required arguments for `routerCreation` is a standardized form of the method that your server uses to execute GraphQL queries. Create a wrapper function labeled `executeFn` that accepts one argument, an object, with four keys: query, context, schema, and variables. Have the wrapper function return the response from the GraphQL server. 142 | 143 | _Example_ 144 | 145 | In this case, the native graphql method from graphql.js is used to execute queries. 146 | 147 | ```javascript 148 | async function executeFn({ query, variables, schema, context, }) { 149 | const data = await graphql(schema, query, null, context, variables); 150 | 151 | return data || errors; 152 | } 153 | ``` 154 | 155 | ### 3) IMPORT 156 | 157 | Lastly, make sure the main Express/GraphQL server file has your GQL schema and context imported in. 158 | 159 | 160 | # Getting Started 161 | 162 | Check out the Example folder within the MONARQ repository to see a simple implementation of the package. 163 | # Keep in Mind 164 | 165 | **+** If default parameters exist in the resolvers, make sure to add the key `defaultParams` with the value of an object with the keys as the variable names and the value of the default value the resolver uses. See the above example [Manifest Object](#required-user-inputs) for more details. 166 | 167 | **+** GraphQL Subscription Type is not supported at this time. 168 | 169 | **+** MONARQ currently supports the 5 main HTTP REST Methods (Get, Post, Put, Patch, Delete). Any other method passed in will throw an error. 170 | 171 | # Contributors 172 | 173 | [Peter Baniuszewicz](https://www.linkedin.com/in/peterbaniuszewicz/) [@Peter-Ba](https://github.com/Peter-Ba) 174 | 175 | [Amy Chen](https://www.linkedin.com/in/amyechen) [@designal46](https://github.com/designal46) 176 | 177 | [Tyler Kneidl](https://www.linkedin.com/in/tylerkneidl/) [@tylerkneidl](https://github.com/tylerkneidl) 178 | 179 | [Helen Regula](https://www.linkedin.com/in/helen-regula/) [@helenregula](https://github.com/helenregula) 180 | 181 | Visit the MONARQ [website](http://monarq.io/) for more information 182 | -------------------------------------------------------------------------------- /__tests__/queryGenTests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* eslint-disable no-undef */ 3 | /* eslint-disable no-unused-vars */ 4 | const { buildSchema } = require('graphql'); 5 | const queryGen = require('../queryMap'); 6 | 7 | const { 8 | queryMap, 9 | generateQuery, 10 | typeChecker, 11 | typeTrim, 12 | grabFields, 13 | buildString, 14 | grabArgs, 15 | argsStrFormatter, 16 | varStrBuild, 17 | } = queryGen; 18 | 19 | const customScalars = ['Date']; 20 | const scalarTypes = [ 21 | 'String', 22 | 'Int', 23 | 'ID', 24 | 'Boolean', 25 | 'Float', 26 | ...customScalars, 27 | ]; 28 | 29 | describe('queryMap function', () => { 30 | it('generates an object with keys args and queries', () => { 31 | const result = queryMap(manifest, schema, ['Date']); 32 | expect(typeof result).toBe('object'); 33 | expect(typeof result.args).toBe('object'); 34 | expect(typeof result.queries).toBe('object'); 35 | }); 36 | 37 | it('throws errors if arguments are not the correct data type', () => { 38 | expect(() => { 39 | queryMap('test', schema, ['Date']); 40 | }).toThrow(Error('manifest argument must an object')); 41 | expect(() => { 42 | queryMap(manifest, 'test', ['Date']); 43 | }).toThrow(Error('schema argument must be a GraphQLSchema object')); 44 | expect(() => { 45 | queryMap(manifest, schema, 'test'); 46 | }).toThrow(Error('customScalars argument must be an array')); 47 | }); 48 | }); 49 | 50 | describe('generateQuery function', () => { 51 | it('handles single scalar arguments', () => { 52 | expect(generateQuery(schema, 'book', scalarTypes)).toBe( 53 | 'query ( $id: ID!, ) { book ( id:$id, ) { id name author { id name publishers { id name createdAt updatedAt } createdAt updatedAt } createdAt updatedAt } }', 54 | ); 55 | }); 56 | 57 | it('handles multiple scalar arguments', () => { 58 | expect(generateQuery(schema, 'books', scalarTypes)).toBe( 59 | 'query ( $pageSize: Int!, $page: Int!, ) { books ( pageSize:$pageSize, page:$page, ) { info { count pages next prev } results { id name author { id name publishers { id name createdAt updatedAt } createdAt updatedAt } createdAt updatedAt } } }', 60 | ); 61 | }); 62 | 63 | it('handles arguments of custom types', () => { 64 | expect(generateQuery(schema, 'updateBook', scalarTypes)).toBe( 65 | 'mutation ( $id: ID!, $name: String, ) { updateBook ( id:$id, book : { name:$name, } ) { id name author { id name publishers { id name createdAt updatedAt } createdAt updatedAt } createdAt updatedAt } }', 66 | ); 67 | }); 68 | 69 | it('throws an error if operation is not defined in schema', () => { 70 | expect(() => { 71 | generateQuery(schema, 'test', scalarTypes); 72 | }).toThrow(Error('Operation \'test\' is not defined in the schema')); 73 | }); 74 | }); 75 | 76 | describe('grabFields function', () => { 77 | it('handles types with all scalar fields', () => { 78 | const recursiveBreak = []; 79 | const customTypeName = 'BookCreateInput'; 80 | expect( 81 | grabFields( 82 | schema, 83 | customTypeName, 84 | schema.getType(customTypeName).getFields(), 85 | recursiveBreak, 86 | scalarTypes, 87 | ), 88 | ).toEqual({ 89 | name: '', 90 | author: '', 91 | }); 92 | }); 93 | 94 | it('returns a nested object with fields corresponding to scalar or custom type, including self-referenced types', () => { 95 | const recursiveBreak = []; 96 | const customTypeName = 'Book'; 97 | expect( 98 | grabFields( 99 | schema, 100 | customTypeName, 101 | schema.getType(customTypeName).getFields(), 102 | recursiveBreak, 103 | scalarTypes, 104 | ), 105 | ).toEqual({ 106 | id: '', 107 | name: '', 108 | author: { 109 | id: '', 110 | name: '', 111 | publishers: { 112 | id: '', 113 | name: '', 114 | createdAt: '', 115 | updatedAt: '', 116 | }, 117 | createdAt: '', 118 | updatedAt: '', 119 | }, 120 | createdAt: '', 121 | updatedAt: '', 122 | }); 123 | }); 124 | }); 125 | 126 | describe('arguments string formatting', () => { 127 | it('handles single scalar arguments', () => { 128 | const argsArr = schema.getQueryType().getFields().book.args; 129 | expect( 130 | argsStrFormatter(buildString(grabArgs(schema, argsArr, scalarTypes)[0])), 131 | ).toBe('id:$id,'); 132 | }); 133 | 134 | it('handles multiple scalar arguments', () => { 135 | const argsArr = schema.getQueryType().getFields().books.args; 136 | expect( 137 | argsStrFormatter(buildString(grabArgs(schema, argsArr, scalarTypes)[0])), 138 | ).toBe('pageSize:$pageSize, page:$page,'); 139 | }); 140 | 141 | it('handles arguments of custom types', () => { 142 | const argsArr = schema.getMutationType().getFields().updateBook.args; 143 | expect( 144 | argsStrFormatter(buildString(grabArgs(schema, argsArr, scalarTypes)[0])), 145 | ).toBe('id:$id, book : { name:$name, }'); 146 | }); 147 | }); 148 | 149 | describe('variables string formatting', () => { 150 | it('handles single scalar arguments', () => { 151 | const argsArr = schema.getQueryType().getFields().book.args; 152 | expect(varStrBuild(grabArgs(schema, argsArr, scalarTypes)[1])).toBe( 153 | '$id: ID!,', 154 | ); 155 | }); 156 | 157 | it('handles multiple scalar arguments', () => { 158 | const argsArr = schema.getQueryType().getFields().books.args; 159 | expect(varStrBuild(grabArgs(schema, argsArr, scalarTypes)[1])).toBe( 160 | '$pageSize: Int!, $page: Int!,', 161 | ); 162 | }); 163 | 164 | it('handles arguments of custom types', () => { 165 | const argsArr = schema.getMutationType().getFields().updateBook.args; 166 | expect(varStrBuild(grabArgs(schema, argsArr, scalarTypes)[1])).toBe( 167 | '$id: ID!, $name: String,', 168 | ); 169 | }); 170 | }); 171 | 172 | /* Manifest and Schema inputs */ 173 | const manifest = { 174 | endpoints: { 175 | '/book/:id': { 176 | get: { 177 | operation: 'book', 178 | }, 179 | post: { 180 | operation: 'updateBook', 181 | }, 182 | }, 183 | '/books': { 184 | post: { 185 | operation: 'books', 186 | }, 187 | }, 188 | }, 189 | }; 190 | 191 | const typeDefs = ` 192 | scalar Date 193 | 194 | type Query { 195 | # Books 196 | books(pageSize: Int!, page: Int!): Books! 197 | book(id: ID!): Book 198 | } 199 | 200 | type Mutation { 201 | # Books 202 | updateBook(id: ID!, book: BookUpdateInput!): Book! 203 | } 204 | 205 | type Book implements Timestamps { 206 | id: ID! 207 | name: String! 208 | author: Author! 209 | 210 | # Interface required 211 | createdAt: Date! 212 | updatedAt: Date! 213 | } 214 | 215 | type Books { 216 | info: Info! 217 | results: [Book]! 218 | } 219 | 220 | # Inputs 221 | input BookCreateInput { 222 | name: String! 223 | author: ID! 224 | } 225 | 226 | input BookUpdateInput { 227 | name: String 228 | } 229 | 230 | type Author implements Timestamps { 231 | id: ID! 232 | name: String! 233 | publishers: [Publisher] 234 | 235 | # Interface required 236 | createdAt: Date! 237 | updatedAt: Date! 238 | } 239 | 240 | type Publisher implements Timestamps { 241 | id: ID! 242 | name: String! 243 | authors: [Author] 244 | # Interface required 245 | createdAt: Date! 246 | updatedAt: Date! 247 | } 248 | 249 | # Interface 250 | interface Timestamps { 251 | createdAt: Date! 252 | updatedAt: Date! 253 | } 254 | 255 | # Types 256 | type Info { 257 | count: Int 258 | pages: Int 259 | next: Int 260 | prev: Int 261 | } 262 | `; 263 | const schema = buildSchema(typeDefs); 264 | -------------------------------------------------------------------------------- /__tests__/routerCreationTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-unused-vars */ 3 | /* eslint-disable no-undef */ 4 | const express = require('express'); 5 | const request = require('supertest'); 6 | 7 | const routerCreation = require('../routerCreation'); 8 | 9 | const app = express(); 10 | 11 | describe('routerCreation Function Test', () => { 12 | const manifest = { 13 | endpoints: { 14 | '/working': { 15 | get: { 16 | operation: 'yes', 17 | }, 18 | }, 19 | }, 20 | }; 21 | 22 | const queryMap = { 23 | args: { 24 | yes: { yes: 'This is the args' }, 25 | }, 26 | queries: { 27 | yes: 'This is the query', 28 | }, 29 | }; 30 | 31 | const executeFn = ({ 32 | query, variables, schema, context, 33 | }) => ({ 34 | success: 'Test successful', 35 | }); 36 | 37 | const badExecuteFn = ({ 38 | query, variables, schema, context, 39 | }) => ({ 40 | errors: 'Test Failed', 41 | }); 42 | const returned = routerCreation(manifest, queryMap, { 43 | schema: 'This is Schema', 44 | context: { 45 | info: 'This is Context', 46 | }, 47 | executeFn, 48 | }); 49 | 50 | const badReturned = routerCreation(manifest, queryMap, { 51 | schema: 'This is Schema', 52 | context: { 53 | info: 'This is Context', 54 | }, 55 | executeFn: badExecuteFn, 56 | }); 57 | 58 | it('should return an express router function', () => { 59 | expect(typeof returned).toEqual('function'); 60 | }); 61 | 62 | it('should have a stack with one element in it', () => { 63 | expect(returned.stack.length).toEqual(1); 64 | }); 65 | 66 | it('should throw error if manifest is inputted wrong', () => { 67 | const badManifest = { 68 | endpoints: {}, 69 | }; 70 | expect(() => routerCreation(badManifest)).toThrow( 71 | Error( 72 | 'manifest is not defined in routeCreation function. Please check documentation for MONARQ on how to pass in the manifest properly', 73 | ), 74 | ); 75 | }); 76 | 77 | describe('Testing express router that is outputted from routerCreation function', () => { 78 | app.use('/test', returned); 79 | app.use('/bad', badReturned); 80 | 81 | it('When request is sent, should send back a status code 200 if inputs were correct', (done) => { 82 | request(app).get('/test/working').expect(200, done); 83 | }); 84 | 85 | it('Should throw error if the executeFn was passed wrong', (done) => { 86 | request(app).get('/bad/working').expect(500, done); 87 | }); 88 | it('Should Contain Response back to client', (done) => { 89 | request(app) 90 | .get('/test/working') 91 | .then((response) => { 92 | expect(response.body.success).toEqual('Test successful'); 93 | done(); 94 | }) 95 | .catch((err) => { 96 | if (err) console.log(err); 97 | }); 98 | }); 99 | 100 | it('Should Contain Error Response back to client', (done) => { 101 | request(app) 102 | .get('/bad/working') 103 | .then((response) => { 104 | expect(response.body).toEqual( 105 | 'Issue Executing Request: undefined', 106 | ); 107 | done(); 108 | }) 109 | .catch(done); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const routerCreation = require('./routerCreation'); 2 | 3 | const queryGen = require('./queryMap'); 4 | 5 | const { queryMap } = queryGen; 6 | 7 | module.exports = { 8 | routerCreation, 9 | queryMap, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monarq", 3 | "version": "1.0.0", 4 | "description": "Rest API to GraphQL queries", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --verbose" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/MONARQ.git" 12 | }, 13 | "author": "Team MONARQ", 14 | "email": "monarq.team@gmail.com", 15 | "contributors": [ 16 | "Peter Baniuszewicz", 17 | "Amy Chen", 18 | "Tyler Kneidl", 19 | "Helen Regula" 20 | ], 21 | "license": "MIT", 22 | "keywords": [ 23 | "api", 24 | "rest", 25 | "restful", 26 | "graphql", 27 | "monarq", 28 | "rest-to-graphql" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/oslabs-beta/MONARQ/issues" 32 | }, 33 | "homepage": "https://github.com/oslabs-beta/MONARQ#readme", 34 | "dependencies": { 35 | "express": "^4.17.1" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^7.27.0", 39 | "eslint-config-airbnb": "^18.2.1", 40 | "eslint-config-airbnb-base": "^14.2.1", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-plugin-import": "^2.23.4", 43 | "eslint-plugin-jsx-a11y": "^6.4.1", 44 | "eslint-plugin-react": "^7.24.0", 45 | "eslint-plugin-react-hooks": "^4.2.0", 46 | "graphql": "^15.5.0", 47 | "jest": "^26.6.3", 48 | "supertest": "^6.1.3" 49 | }, 50 | "prettier": { 51 | "singleQuote": true 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /queryMap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable no-use-before-define */ 3 | /* eslint-disable max-len */ 4 | /** ****************************************************** 5 | ***** ARGS & QUERY OBJECT OUTPUT FUNCTION *************** 6 | ******************************************************* */ 7 | 8 | /** 9 | * 10 | * @param {*} manifest: the manifest object created by the user, which contains REST endpoints and corresponding GraphQL operations 11 | * @param {*} schema: the schema that contains all relevant GraphQL operations and types, including those specified in the manifest; must be a GraphQLSchema object 12 | * @param {*} customScalars: an array containing all custom scalar types that exist in the schema argument; each element should be a custom scalar type and in string format (e.g., ['Month', 'Year']) 13 | // if no custom scalar types are used in the schema, then do not pass an argument for this parameter 14 | * @returns: an object containing the two keys below; this is an input into the routerCreation function. 15 | // args: an object composed of key-value pairs for any operations (keys) that have argument inputs (values); note that operations without argument inputs will not be included here 16 | // queries: an object composed of key-value pairs for all operations (keys) specified in the manifest and the corresponding GraphQL schema (values) to be invoked 17 | */ 18 | 19 | const queryMap = (manifest, schema, customScalars = []) => { 20 | if (typeof manifest !== 'object') { 21 | throw new Error('manifest argument must an object'); 22 | } 23 | if (typeof schema !== 'object') { 24 | throw new Error('schema argument must be a GraphQLSchema object'); 25 | } 26 | if (typeof customScalars !== 'object') { 27 | throw new Error('customScalars argument must be an array'); 28 | } 29 | 30 | const endPoints = manifest.endpoints; 31 | const argsObj = {}; 32 | const queryObj = {}; 33 | const scalarTypes = [ 34 | 'String', 35 | 'Int', 36 | 'ID', 37 | 'Boolean', 38 | 'Float', 39 | ...customScalars, 40 | ]; 41 | for (const path of Object.keys(endPoints)) { 42 | for (const action of Object.keys(endPoints[path])) { 43 | const operationName = endPoints[path][action].operation; 44 | // generate the args object 45 | const typeSchema = typeChecker(schema, operationName)[1]; 46 | const operationFields = typeSchema[operationName]; 47 | const varObj = grabArgs(schema, operationFields.args, scalarTypes)[1]; 48 | argsObj[operationName] = varObj; 49 | 50 | // generate the query object 51 | queryObj[operationName] = generateQuery( 52 | schema, 53 | operationName, 54 | scalarTypes, 55 | ); 56 | } 57 | } 58 | return { 59 | args: argsObj, 60 | queries: queryObj, 61 | }; 62 | }; 63 | 64 | /** ******************************** 65 | ***** QUERY GENERATOR FUNCTION **** 66 | ********************************* */ 67 | // This is invoked within queryMap; can also be used to generate the GraphQL fully expanded query string for any single operation 68 | 69 | /** 70 | * 71 | * @param {*} schema: same schema that is passed into queryMap 72 | * @param {*} operation: a GraphQL operation that has been mapped to a REST endpoint within the manifest object 73 | * @param {*} scalarTypes: an array containing all standard scalar types as well as any custom scalar types that have been declared in the schema (scalarTypes is declared in queryMap and this is a pass-through parameter) 74 | * @returns: a string comprising the GraphQL query that corresponds to the operation argument; this query will be passed to the GraphQL server each time a request is made to the corresponding REST endpoint 75 | // The query string includes whichever parts are relevant to the operation, including the operation type, variables specification, operation name, arguments specification, fields specification (all available fields are requested) 76 | */ 77 | 78 | const generateQuery = (schema, operation, scalarTypes) => { 79 | // determine whether it is a query or mutation 80 | const typeInfo = typeChecker(schema, operation); 81 | const operationType = typeInfo[0]; 82 | const typeSchema = typeInfo[1]; 83 | 84 | // look for all of the fields that need to be specified for the operation 85 | let returnFields = {}; 86 | const operationFields = typeSchema[operation]; 87 | let customTypeFields; 88 | let customType; 89 | const recursiveBreak = []; 90 | 91 | // check to see if the type is a scalar type -> if not, then need to look up the fields for each type 92 | const operationFieldsTypeTrim = typeTrim(operationFields.type.toString()); 93 | if (scalarTypes.includes(operationFieldsTypeTrim)) { 94 | returnFields[operationFieldsTypeTrim] = ''; 95 | } else { 96 | customType = operationFields.type; 97 | customTypeFields = schema 98 | .getType(typeTrim(operationFields.type.toString())) 99 | .getFields(); 100 | returnFields = grabFields( 101 | schema, 102 | customType, 103 | customTypeFields, 104 | recursiveBreak, 105 | scalarTypes, 106 | ); 107 | } 108 | const queryString = buildString(returnFields); 109 | 110 | // compose the variable and argument portions of the query if necessary 111 | let argsString; 112 | let varsString; 113 | if (operationFields.args.length) { 114 | const argsObj = grabArgs(schema, operationFields.args, scalarTypes)[0]; 115 | const argsVal = argsStrFormatter(buildString(argsObj)); 116 | argsString = `( ${argsVal} )`; 117 | const varObj = grabArgs(schema, operationFields.args, scalarTypes)[1]; 118 | const varsVal = varStrBuild(varObj); 119 | varsString = `( ${varsVal} )`; 120 | argsObj[operation] = argsVal; 121 | } else { 122 | argsString = ''; 123 | varsString = ''; 124 | } 125 | 126 | return `${operationType.toLowerCase()} ${varsString} { ${operation} ${argsString} { ${queryString} } }`; 127 | }; 128 | 129 | /** ************************ 130 | ***** ADDITIONAL FUNCTIONS 131 | ************************** */ 132 | 133 | /* determines whether the operation is a query or mutation */ 134 | const typeChecker = (schema, operation) => { 135 | let operationType; 136 | let typeSchema; 137 | const querySchema = schema.getQueryType().getFields(); 138 | const mutationSchema = schema.getMutationType().getFields(); 139 | if (Object.keys(querySchema).includes(operation)) { 140 | operationType = 'Query'; 141 | typeSchema = querySchema; 142 | } 143 | if (Object.keys(mutationSchema).includes(operation)) { 144 | operationType = 'Mutation'; 145 | typeSchema = mutationSchema; 146 | } 147 | if (!operationType) { 148 | throw new Error(`Operation '${operation}' is not defined in the schema`); 149 | } 150 | return [operationType, typeSchema]; 151 | }; 152 | 153 | /* converts custom type text to simple strings */ 154 | const typeTrim = (type) => { 155 | const typeArr = type.split(''); 156 | const trimArr = []; 157 | for (let i = 0; i < typeArr.length; i += 1) { 158 | if (typeArr[i] !== '[' && typeArr[i] !== ']' && typeArr[i] !== '!') { 159 | trimArr.push(typeArr[i]); 160 | } 161 | } 162 | return trimArr.join(''); 163 | }; 164 | 165 | /* grabFields collects all of the fields associated with a type; if the field is scalar type, it adds to the return object; 166 | if the field is a custom type, the function is invoked again on that field's schema fields continues recursively until only scalar types are found 167 | countOccurrences is used to track the number of times each customType has been called */ 168 | const countOccurrences = (array, val) => array.reduce((a, v) => (v === val ? a + 1 : a), 0); 169 | 170 | const grabFields = ( 171 | schema, 172 | customTypeName, 173 | customTypeSchema, 174 | recursiveBreakArr, 175 | scalarTypes, 176 | ) => { 177 | const returnObj = {}; 178 | for (const key of Object.keys(customTypeSchema)) { 179 | const typeString = typeTrim(customTypeSchema[key].type.toString()); 180 | if (scalarTypes.includes(typeString)) returnObj[key] = ''; 181 | else { 182 | recursiveBreakArr.push(typeString); 183 | if (countOccurrences(recursiveBreakArr, typeString) < 2) { 184 | returnObj[key] = grabFields( 185 | schema, 186 | typeString, 187 | schema.getType(typeString).getFields(), 188 | recursiveBreakArr, 189 | scalarTypes, 190 | ); 191 | } 192 | } 193 | } 194 | return returnObj; 195 | }; 196 | 197 | /* convert the query/args object to string version; called recursively if there are nested type objs */ 198 | const buildString = (fieldsObj) => { 199 | const queryArr = []; 200 | for (const key of Object.keys(fieldsObj)) { 201 | queryArr.push(key); 202 | if (fieldsObj[key] !== '') { 203 | queryArr.push('{'); 204 | queryArr.push(buildString(fieldsObj[key])); 205 | queryArr.push('}'); 206 | } 207 | } 208 | return queryArr.join(' '); 209 | }; 210 | 211 | /* collects all of the arguments, handling all potential cases: 212 | 1) single scalar arg 213 | 2) multiple scalar args 214 | 3) custom input types as args */ 215 | const grabArgs = (schema, argsArr, scalarTypes) => { 216 | const returnArgsObj = {}; 217 | const returnVarsObj = {}; 218 | for (let i = 0; i < argsArr.length; i += 1) { 219 | const typeString = typeTrim(argsArr[i].type.toString()); 220 | if (scalarTypes.includes(typeString)) { 221 | returnArgsObj[argsArr[i].name] = ''; 222 | returnVarsObj[`$${argsArr[i].name}`] = argsArr[i].type.toString(); 223 | } else { 224 | const nestedFields = grabFields( 225 | schema, 226 | typeString, 227 | schema.getType(typeString).getFields(), 228 | [], 229 | scalarTypes, 230 | ); 231 | returnArgsObj[argsArr[i].name] = nestedFields; 232 | for (const field of Object.keys(nestedFields)) { 233 | returnVarsObj[`$${field}`] = schema.getType(typeString).getFields()[ 234 | field 235 | ].type; 236 | } 237 | } 238 | } 239 | return [returnArgsObj, returnVarsObj]; 240 | }; 241 | 242 | /* formats the args string into the arg:$arg format */ 243 | const argsStrFormatter = (str) => { 244 | const strArray = str.split(' '); 245 | const insIndex = strArray.indexOf('{'); 246 | if (insIndex > 0) { 247 | for (let i = insIndex + 1; i < strArray.length - 1; i += 1) { 248 | strArray[i] = `${strArray[i]}:$${strArray[i]},`; 249 | } 250 | if (insIndex > 1) strArray[0] = `${strArray[0]}:$${strArray[0]},`; 251 | strArray.splice(insIndex, 0, ':'); 252 | } else { 253 | for (let i = 0; i < strArray.length; i += 1) { 254 | strArray[i] = `${strArray[i]}:$${strArray[i]},`; 255 | } 256 | } 257 | return strArray.join(' '); 258 | }; 259 | 260 | /* formats the args string into the $var: type format for variables */ 261 | const varStrBuild = (varObj) => { 262 | const varArr = []; 263 | for (const key of Object.keys(varObj)) { 264 | varArr.push(`${key}:`); 265 | varArr.push(`${varObj[key]},`); 266 | } 267 | return varArr.join(' '); 268 | }; 269 | 270 | module.exports = { 271 | queryMap, 272 | generateQuery, 273 | typeChecker, 274 | typeTrim, 275 | grabFields, 276 | buildString, 277 | grabArgs, 278 | argsStrFormatter, 279 | varStrBuild, 280 | }; 281 | -------------------------------------------------------------------------------- /routerCreation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | /* eslint-disable spaced-comment */ 3 | /* eslint-disable no-use-before-define */ 4 | /* eslint-disable max-len */ 5 | const express = require('express'); 6 | 7 | /** 8 | * routerCreation: Function: takes in three arguments and returns and instance of an express.Router. The user would then save the returned result as a variable to use in their GraphQL/Express server to route all REST requests through. 9 | * @param {Object} manifest: Created by the user. The first key of the manifest object should be 'endpoints'. See the webiste to create your own manifest object easily or look at the readMe on the Github repo for how to format the manifest object properly. 10 | * @param {Object} createdGQL: Output from the function queryMap. 11 | * @param {Object} infoForExecution: Has three keys: 12 | * { 13 | * schema: GQL Schema 14 | * context: Object, or a Function that returns an object: this is passed to the executeFn for the GraphQL resolvers. Here the function passes in the req.headers from the request object as 'headers' so the resolvers have access to the headers for any authorization needs. 15 | * executeFn: Function: this is a wrapped function created by the user that will return the response from your GraphQL/Express server. Have the function return the GraphQL response object in it's entirety. The wrapped function will accept one object that will have four keys: 16 | * { 17 | * query: String: specific query/mutation from the object outputted by queryMap, 18 | * variables: Object or Null: all necessary variables from the request object, if no variables are required, passed in or no defaultParams are defined in the manifest object, the variables value will be null, 19 | * schema: GQL Schema: passed into infoForExecution object, 20 | * context: Object: this was initally passed into routerCreation's infoForExecution object as either a Function that returns an object or an Object plainly. routerCreation will pass the request object's headers into the context object with the key of 'headers' for the resolvers to have access to. 21 | * } 22 | * } 23 | * 24 | * @returns an instance of an express.Router() populated with all the REST API paths that the user defined in the manifest object. 25 | */ 26 | 27 | const routerCreation = (manifest, createdGQL, infoForExecution) => { 28 | // Creates the instance of the express Router. 29 | const router = express.Router(); 30 | 31 | // This function returns the manifest object if it passes the check to make sure manifest is passed in and defined correctly. 32 | // eslint-disable-next-line no-use-before-define 33 | const endPointObj = validateManifest(manifest); 34 | 35 | const { endpoints } = endPointObj; 36 | 37 | // Loop through each apiPath and then loops through each method within the specific apiPath. 38 | Object.keys(endpoints).forEach((apiPath) => { 39 | Object.keys(endpoints[apiPath]).forEach((method) => { 40 | // eslint-disable-next-line prefer-const 41 | let { queries, args } = createdGQL; 42 | const { defaultParams } = endpoints[apiPath][method]; 43 | let currentQuery; 44 | 45 | // Taking the args object from the passed createdGQL object (which is the returned result of the function queryMap), and reassigning args to hold the specific arguments object that holds all the required arguments for the GraphQL operation. 46 | args = args[endpoints[apiPath][method].operation]; 47 | 48 | // Matching the operation in the manifest object to the query/ mutation that matches in the createdGQL.queries (which holds the GQL string) that will be passed into the executeFn. 49 | Object.keys(queries).forEach((query) => { 50 | if (query === endpoints[apiPath][method].operation) { 51 | currentQuery = queries[query]; 52 | } 53 | }); 54 | 55 | // If the the operation field in the manifest object didn't match any query or mutation in the createdGQL.queries object- throw an error. 56 | if (!currentQuery) { 57 | throw new Error( 58 | 'Manifest Obj \'s Operation Field Doesn\'t match Valid Query or Mutation in Schema. Operation Field is Mandatory in Manifest Obj for every method. Check the operation field in the Manifest Object. Visit our website to create a manifest object', 59 | ); 60 | } 61 | 62 | // This function is invoked in every loop of the apiPath and method. This function will add all the routes to the express.Router declared above. 63 | addRoutes( 64 | method, 65 | apiPath, 66 | currentQuery, 67 | router, 68 | args, 69 | defaultParams, 70 | infoForExecution, 71 | ); 72 | }); 73 | }); 74 | 75 | return router; 76 | }; 77 | 78 | ///////////////////////////////// 79 | ///// ///// 80 | ///// Additional Functions ///// 81 | ///// ///// 82 | ///////////////////////////////// 83 | 84 | /* validateManifest 85 | Accepts the manifest object created by the user and returns the object as long as at least one apiPath is instantiated inside the object. 86 | */ 87 | const validateManifest = (manifestObj) => { 88 | if (Object.keys(manifestObj.endpoints).length < 1) { 89 | throw new Error( 90 | 'manifest is not defined in routeCreation function. Please check documentation for MONARQ on how to pass in the manifest properly', 91 | ); 92 | } 93 | 94 | return manifestObj; 95 | }; 96 | 97 | /* populateVariables 98 | Accepts: 99 | requiredVariables: Object, 100 | defaultParams: Object or Null: this is defined in the manifest object 101 | reqObj: Object: a created object that holds the request object's params, query and body values from the express middleware. 102 | If the query has no required variables, then the value of variables will be null. If not, populateVariables will check if the key in required variables matches the reqObj keys, if it does then the variable will be added to the variables object. This was done to increase security for our users so no client can send extraneous variables that will be passed into the user's GraphQL API. 103 | The return of the function checks if variables is populated, if not, it checks in the defaultParams have a value in it, and if not returns variables to be null. 104 | */ 105 | const populateVariables = (requiredVariables, defaultParams, reqObj) => { 106 | if (requiredVariables === undefined || requiredVariables === null) { 107 | return null; 108 | } 109 | 110 | const variables = {}; 111 | 112 | Object.keys(requiredVariables).forEach((key) => { 113 | Object.keys(reqObj).forEach((keyMatch) => { 114 | if (key === `$${keyMatch}`) { 115 | variables[keyMatch] = reqObj[keyMatch]; 116 | } 117 | }); 118 | }); 119 | 120 | // eslint-disable-next-line no-nested-ternary 121 | return Object.keys(variables).length > 0 122 | ? variables 123 | : defaultParams !== undefined || defaultParams !== null 124 | ? defaultParams 125 | : null; 126 | }; 127 | 128 | /** 129 | * addRoutes: Function that adds the routes to the express.Router 130 | * @param {String} method: Associated with the key in the manifest object's apiPath object. The only supported HTTP Methods our package supports is GET, POST, PUT, PATCH, and DELETE. 131 | * @param {String} apiPath: Associated with the key in the manifest object's endpoint object 132 | * @param {String} GQLquery: Associated with the query/mutation string from the createdGQL.queries object. 133 | * @param {express.Router} router: This will be passed in every invocation that will add the routes to the express.Router 134 | * @param {Object} argsForQuery: This holds the required arguments for the GQLquery string if it exists 135 | * @param {Object} defaultParams: Declared in the manifest object within the method object. Only required to specify this field in the manifest object if the user's resolver has default parameters. Our function will overwrite those default parameters. 136 | * @param {Object} infoForExecution: The object that was initially passed into routerCreation that will be used to execute the query/mutation string inside the users GraphQL API. 137 | * 138 | */ 139 | 140 | const addRoutes = ( 141 | method, 142 | apiPath, 143 | GQLquery, 144 | router, 145 | argsForQuery, 146 | defaultParams, 147 | infoForExecution, 148 | ) => { 149 | // checks the method string from the manifest object 150 | switch (method.toLowerCase()) { 151 | case 'get': { 152 | router.get(apiPath, async (req, res) => { 153 | const { query, params, body } = req; 154 | 155 | // Order does matter here, if req.query has the same key as req.params or req.body, it will be overwritten when params or body is spread out in possibleInputs object. 156 | const possibleInputs = { 157 | ...query, 158 | ...params, 159 | ...body, 160 | }; 161 | 162 | const { schema, context, executeFn } = infoForExecution; 163 | 164 | // Security check to ensure the variables passed into the GraphQL API will only contain the required variables for that query/mutation. 165 | const variables = populateVariables( 166 | argsForQuery, 167 | defaultParams, 168 | possibleInputs, 169 | ); 170 | 171 | // Checking if context is a function or an object. Then adds the req.headers to the newContext object so the resolvers have access to the headers. It will be saved under the key 'headers'. 172 | let newContext; 173 | 174 | if (typeof context === 'function') { 175 | newContext = await context(); 176 | newContext.headers = req.headers; 177 | } else if (typeof context === 'object') { 178 | newContext = context; 179 | newContext.headers = req.headers; 180 | } 181 | 182 | // If context was neither an object or a function that returns an object, an error will be thrown 183 | if (!newContext || typeof newContext !== 'object') { 184 | throw new Error( 185 | 'Context was not passed in correctly, could not execute the query. Make sure context is either a function that returns an object or an object plainly. Please check the documentation on the MONARQ repo or the website for further understanding.', 186 | ); 187 | } 188 | 189 | // Creating the object that will be passed into the executeFn the user defined. 190 | const executeObj = { 191 | query: GQLquery, 192 | variables, 193 | schema, 194 | context: newContext, 195 | }; 196 | 197 | // Execute the function that will return the response from the GraphQL API. 198 | const response = await executeFn(executeObj); 199 | 200 | if (response.errors) { 201 | res 202 | .status(500) 203 | .json(`Issue Executing Request: ${response.errors[0].message}`); 204 | // eslint-disable-next-line no-console 205 | console.warn(`${response.errors}`); 206 | return; 207 | } 208 | 209 | // The whole response from the GraphQL API will now be saved in the response object. 210 | res.locals.data = response; 211 | 212 | // Then the client will be served the GraphQL response object. 213 | return res.status(200).json(res.locals.data); 214 | }); 215 | 216 | break; 217 | } 218 | 219 | case 'delete': { 220 | router.delete(apiPath, async (req, res) => { 221 | const { query, params, body } = req; 222 | 223 | // Order does matter here, if req.query has the same key as req.params or req.body, it will be overwritten when params or body is spread out in possibleInputs object. 224 | const possibleInputs = { 225 | ...query, 226 | ...params, 227 | ...body, 228 | }; 229 | 230 | const { schema, context, executeFn } = infoForExecution; 231 | 232 | // Security check to ensure the variables passed into the GraphQL API will only contain the required variables for that query/mutation. 233 | const variables = populateVariables( 234 | argsForQuery, 235 | defaultParams, 236 | possibleInputs, 237 | ); 238 | 239 | // Checking if context is a function or an object. Then adds the req.headers to the newContext object so the resolvers have access to the headers. It will be saved under the key 'headers'. 240 | let newContext; 241 | 242 | if (typeof context === 'function') { 243 | newContext = await context(); 244 | newContext.headers = req.headers; 245 | } else if (typeof context === 'object') { 246 | newContext = context; 247 | newContext.headers = req.headers; 248 | } 249 | 250 | // If context was neither an object or a function that returns an object, an error will be thrown. 251 | if (!newContext || typeof newContext !== 'object') { 252 | throw new Error( 253 | 'Context was not passed in correctly, could not execute the query. Make sure context is either a function that returns an object or an object plainly. Please check the documentation on the MONARQ repo or the website for further understanding.', 254 | ); 255 | } 256 | 257 | // Creating the object that will be passed into the executeFn the user defined. 258 | const executeObj = { 259 | query: GQLquery, 260 | variables, 261 | schema, 262 | context: newContext, 263 | }; 264 | 265 | // Execute the function that will return the response from the GraphQL API. 266 | const response = await executeFn(executeObj); 267 | 268 | // If the errors field exists in the response object, client will be notified and the error will log to the console. 269 | if (response.errors) { 270 | res 271 | .status(500) 272 | .json(`Issue Executing Request: ${response.errors[0].message}`); 273 | // eslint-disable-next-line no-console 274 | console.warn(`${response.errors}`); 275 | return; 276 | } 277 | 278 | // The whole response from the GraphQL API will now be saved in the response object. 279 | res.locals.data = response; 280 | 281 | // Then the client will be served the GraphQL response object. 282 | // eslint-disable-next-line consistent-return 283 | return res.status(200).json(res.locals.data); 284 | }); 285 | 286 | break; 287 | } 288 | 289 | case 'post': { 290 | router.post(apiPath, async (req, res) => { 291 | const { query, params, body } = req; 292 | 293 | // Order does matter here, if req.query has the same key as req.params or req.body, it will be overwritten when params or body is spread out in possibleInputs object. 294 | const possibleInputs = { 295 | ...query, 296 | ...params, 297 | ...body, 298 | }; 299 | 300 | const { schema, context, executeFn } = infoForExecution; 301 | 302 | // Security check to ensure the variables passed into the GraphQL API will only contain the required variables for that query/mutation. 303 | const variables = populateVariables( 304 | argsForQuery, 305 | defaultParams, 306 | possibleInputs, 307 | ); 308 | 309 | // Checking if context is a function or an object. Then adds the req.headers to the newContext object so the resolvers have access to the headers. It will be saved under the key 'headers'. 310 | let newContext; 311 | 312 | if (typeof context === 'function') { 313 | newContext = await context(); 314 | newContext.headers = req.headers; 315 | } else if (typeof context === 'object') { 316 | newContext = context; 317 | newContext.headers = req.headers; 318 | } 319 | 320 | // If context was neither an object or a function that returns an object, an error will be thrown. 321 | if (!newContext || typeof newContext !== 'object') { 322 | throw new Error( 323 | 'Context was not passed in correctly, could not execute the query. Make sure context is either a function that returns an object or an object plainly. Please check the documentation on the MONARQ repo or the website for further understanding.', 324 | ); 325 | } 326 | 327 | // Creating the object that will be passed into the executeFn the user defined. 328 | const executeObj = { 329 | query: GQLquery, 330 | variables, 331 | schema, 332 | context: newContext, 333 | }; 334 | 335 | // Execute the function that will return the response from the GraphQL API. 336 | const response = await executeFn(executeObj); 337 | 338 | // If the errors field exists in the response object, client will be notified and the error will log to the console. 339 | if (response.errors) { 340 | res 341 | .status(500) 342 | .json(`Issue Executing Request: ${response.errors[0].message}`); 343 | // eslint-disable-next-line no-console 344 | console.warn(`${response.errors}`); 345 | return; 346 | } 347 | 348 | // The whole response from the GraphQL API will now be saved in the response object. 349 | res.locals.data = response; 350 | 351 | // Then the client will be served the GraphQL response object. 352 | // eslint-disable-next-line consistent-return 353 | return res.status(200).json(res.locals.data); 354 | }); 355 | 356 | break; 357 | } 358 | 359 | case 'put': { 360 | router.put(apiPath, async (req, res) => { 361 | const { query, params, body } = req; 362 | 363 | // Order does matter here, if req.query has the same key as req.params or req.body, it will be overwritten when params or body is spread out in possibleInputs object. 364 | const possibleInputs = { 365 | ...query, 366 | ...params, 367 | ...body, 368 | }; 369 | 370 | const { schema, context, executeFn } = infoForExecution; 371 | 372 | // Security check to ensure the variables passed into the GraphQL API will only contain the required variables for that query/mutation. 373 | const variables = populateVariables( 374 | argsForQuery, 375 | defaultParams, 376 | possibleInputs, 377 | ); 378 | 379 | // Checking if context is a function or an object. Then adds the req.headers to the newContext object so the resolvers have access to the headers. It will be saved under the key 'headers'. 380 | let newContext; 381 | 382 | if (typeof context === 'function') { 383 | newContext = await context(); 384 | newContext.headers = req.headers; 385 | } else if (typeof context === 'object') { 386 | newContext = context; 387 | newContext.headers = req.headers; 388 | } 389 | 390 | // If context was neither an object or a function that returns an object, an error will be thrown. 391 | if (!newContext || typeof newContext !== 'object') { 392 | throw new Error( 393 | 'Context was not passed in correctly, could not execute the query. Make sure context is either a function that returns an object or an object plainly. Please check the documentation on the MONARQ repo or the website for further understanding.', 394 | ); 395 | } 396 | 397 | // Creating the object that will be passed into the executeFn the user defined. 398 | const executeObj = { 399 | query: GQLquery, 400 | variables, 401 | schema, 402 | context: newContext, 403 | }; 404 | 405 | // Execute the function that will return the response from the GraphQL API. 406 | const response = await executeFn(executeObj); 407 | 408 | // If the errors field exists in the response object, client will be notified and the error will log to the console. 409 | if (response.errors) { 410 | res 411 | .status(500) 412 | .json(`Issue Executing Request: ${response.errors[0].message}`); 413 | // eslint-disable-next-line no-console 414 | console.warn(`${response.errors}`); 415 | return; 416 | } 417 | 418 | // The whole response from the GraphQL API will now be saved in the response object. 419 | res.locals.data = response; 420 | 421 | // Then the client will be served the GraphQL response object. 422 | return res.status(200).json(res.locals.data); 423 | }); 424 | 425 | break; 426 | } 427 | 428 | case 'patch': { 429 | router.patch(apiPath, async (req, res) => { 430 | const { query, params, body } = req; 431 | 432 | // Order does matter here, if req.query has the same key as req.params or req.body, it will be overwritten when params or body is spread out in possibleInputs object. 433 | const possibleInputs = { 434 | ...query, 435 | ...params, 436 | ...body, 437 | }; 438 | 439 | const { schema, context, executeFn } = infoForExecution; 440 | 441 | // Security check to ensure the variables passed into the GraphQL API will only contain the required variables for that query/mutation. 442 | const variables = populateVariables( 443 | argsForQuery, 444 | defaultParams, 445 | possibleInputs, 446 | ); 447 | 448 | // Checking if context is a function or an object. Then adds the req.headers to the newContext object so the resolvers have access to the headers. It will be saved under the key 'headers'. 449 | let newContext; 450 | 451 | if (typeof context === 'function') { 452 | newContext = await context(); 453 | newContext.headers = req.headers; 454 | } else if (typeof context === 'object') { 455 | newContext = context; 456 | newContext.headers = req.headers; 457 | } 458 | 459 | // If context was neither an object or a function that returns an object, an error will be thrown. 460 | if (!newContext || typeof newContext !== 'object') { 461 | throw new Error( 462 | 'Context was not passed in correctly, could not execute the query. Make sure context is either a function that returns an object or an object plainly. Please check the documentation on the MONARQ repo or the website for further understanding.', 463 | ); 464 | } 465 | 466 | // Creating the object that will be passed into the executeFn the user defined. 467 | const executeObj = { 468 | query: GQLquery, 469 | variables, 470 | schema, 471 | context: newContext, 472 | }; 473 | 474 | // Execute the function that will return the response from the GraphQL API. 475 | const response = await executeFn(executeObj); 476 | 477 | // If the errors field exists in the response object, client will be notified and the error will log to the console. 478 | if (response.errors) { 479 | res 480 | .status(500) 481 | .json(`Issue Executing Request: ${response.errors[0].message}`); 482 | // eslint-disable-next-line no-console 483 | console.warn(`${response.errors}`); 484 | return; 485 | } 486 | 487 | // The whole response from the GraphQL API will now be saved in the response object. 488 | res.locals.data = response; 489 | 490 | // Then the client will be served the GraphQL response object. 491 | return res.status(200).json(res.locals.data); 492 | }); 493 | 494 | break; 495 | } 496 | 497 | // If the method doesn't match the supported HTTP methods of GET, POST, PUT, PATCH or DELETE, an error will be thrown. 498 | default: 499 | throw new Error( 500 | 'Operation Doesn\'t match the HTTP Methods allowed for this NPM Package, Please see documentation on which HTTP Methods are allowed and/or check the Manifest Object\'s Method Object', 501 | ); 502 | } 503 | }; 504 | 505 | module.exports = routerCreation; 506 | --------------------------------------------------------------------------------