├── .npmignore ├── __tests__ ├── server │ ├── traql.js │ ├── newAqlPayload.js │ ├── analyticsRouter.js │ ├── traqlAudit.js │ └── newTraqlEntry.js └── client │ └── aqlQueryParser.js ├── .gitignore ├── src ├── index.js └── server │ ├── newTraqlEntry.js │ ├── traql.js │ ├── analyticsRouter.js │ ├── newAqlPayload.js │ └── traqlAudit.js ├── package.json ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__tests__/server/traql.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__tests__/server/newAqlPayload.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__tests__/client/aqlQueryParser.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /__tests__/server/analyticsRouter.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /__tests__/server/traqlAudit.js: -------------------------------------------------------------------------------- 1 | const traqlAudit = require('../../server/traqlAudit'); 2 | 3 | describe('Test traqlAudit functionality', () => { 4 | it('should only call once if in process', () => { 5 | const testTraql = { a: 1, b: 2 }; 6 | traqlAudit(testTraql); 7 | traqlAudit(testTraql); 8 | expect(traqlAudit.toHaveBeenCalledTimes(1)); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | //import server modules 2 | const newAqlPayload = require('./server/newAqlPayload'); 3 | const analyticsRouter = require('./server/analyticsRouter'); 4 | const newTraqlEntry = require('./server/newTraqlEntry'); 5 | const Traql = require('./server/traql'); 6 | const traqlAudit = require('./server/traqlAudit'); 7 | 8 | // export all modules 9 | module.exports = { 10 | analyticsRouter, 11 | newAqlPayload, 12 | newTraqlEntry, 13 | Traql, 14 | traqlAudit, 15 | }; 16 | -------------------------------------------------------------------------------- /src/server/newTraqlEntry.js: -------------------------------------------------------------------------------- 1 | /* Create a new entry in the traql that has a mutationId as its argument and an object as its key. 2 | The object will have the resolver data, a timestamp, the expected number of aqls, and the user token. */ 3 | 4 | function newTraqlEntry(traql, args, pubsub) { 5 | traql[args.aql.mutationId] = { 6 | resolver: args.aql.resolver, 7 | openedTime: Date.now(), 8 | expectedNumberOfAqls: Math.floor( 9 | Object.keys(pubsub.subscriptions).length / traql.subResolvers 10 | ), 11 | aqlsReceivedBack: [], 12 | userToken: traql.userToken, 13 | }; 14 | } 15 | module.exports = newTraqlEntry; 16 | -------------------------------------------------------------------------------- /src/server/traql.js: -------------------------------------------------------------------------------- 1 | /* New instance of Traql object, which will be placeholder for Traql entries. 2 | Traql will keep track of the number of subscription resolvers in the system, 3 | which will be used to calculate the number of current subscribers 4 | (subscriptions divided by number of subscription resolvers). */ 5 | 6 | function Traql(resolvers, userToken) { 7 | // Create subResolvers property that is equal to number of subscription resolvers in system. 8 | this.subResolvers = Object.keys(resolvers.Subscription).length; 9 | // Create userToken property 10 | this.userToken = userToken; 11 | } 12 | 13 | module.exports = Traql; 14 | -------------------------------------------------------------------------------- /src/server/analyticsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const timesyncServer = require('timesync/server'); 4 | 5 | /* Looks for mutation ID in the traql object and pushes the number 6 | of aqls received back pertaining to its mutation ID */ 7 | 8 | function analyticsRouter(traql) { 9 | router.post( 10 | '/', 11 | (req, res, next) => { 12 | traql[req.body.mutationId].aqlsReceivedBack.push(req.body); 13 | return next(); 14 | }, 15 | (req, res) => { 16 | res.sendStatus(200); 17 | } 18 | ); 19 | router.post('/aqlsanalytics/timesync', timesyncServer.requestHandler); 20 | return router; 21 | } 22 | 23 | module.exports = analyticsRouter; 24 | -------------------------------------------------------------------------------- /src/server/newAqlPayload.js: -------------------------------------------------------------------------------- 1 | const newTraqlEntry = require('./newTraqlEntry'); 2 | 3 | /* Creates a copy of the received payload, adds AQL with mutationReceived property of the 4 | current time, creates newTraqlEntry for this mutation, and finally returns updated payload. */ 5 | 6 | function newAqlPayload(payload, args, traql, pubsub) { 7 | const newPayload = { ...payload }; 8 | // Update payload to include Aql with mutationReceived and userToken property 9 | for (let key in newPayload) { 10 | newPayload[key].aql = { 11 | ...args.aql, 12 | mutationReceived: Date.now(), 13 | userToken: traql.userToken, 14 | }; 15 | } 16 | // Create new entry in Traql for this mutation 17 | newTraqlEntry(traql, args, pubsub); 18 | // Return updated payload 19 | return newPayload; 20 | } 21 | 22 | module.exports = newAqlPayload; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aqls/server", 3 | "version": "1.0.5", 4 | "description": "An intelligent full-stack GraphQL subscription and analytics module. Server-side analytics processing, self-auditing router, and resolver plugins.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "test", 8 | "build": "webpack --mode production" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/oslabs-beta/Aqls-server" 13 | }, 14 | "keywords": [ 15 | "GraphQL", 16 | "Apollo", 17 | "Apollo-Server", 18 | "Apollo-Client", 19 | "subscriptions", 20 | "analytics" 21 | ], 22 | "author": "AqlOrg", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/oslabs-beta/Aqls-server/issues" 26 | }, 27 | "homepage": "https://aqls.io", 28 | "dependencies": { 29 | "axios": "^0.20.0", 30 | "express": "^4.17.1", 31 | "graphql": "^15.3.0", 32 | "node": "^14.14.0", 33 | "timesync": "^1.0.8", 34 | "uuid": "^8.3.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /src/server/traqlAudit.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /* traqlAudit runs through each traql entry to audit if all expected aqls have been 4 | received back. If they have, it sends aqls to aql-monitor for storage; otherwise 5 | if there is an error, it marks the entry as "on probation"; if an entry is on 6 | probation and all aqls have not been returned, an error entry is sent to aql-monitor. */ 7 | 8 | function traqlAudit(traql) { 9 | let aqlsToBeSent = { 10 | successAqls: [], 11 | errorAqls: [], 12 | }; 13 | let open = true; 14 | if (open) { 15 | open = false; 16 | // if there are any untracked traql entries to be sent to the server 17 | if (Object.keys(traql).length > 2) { 18 | let postReq = { 19 | method: 'post', 20 | url: 'https://www.aqls.io/aqls', 21 | }; 22 | // loop through the untracked traql entries 23 | for (let key in traql) { 24 | // if it's not the subResolver or UserToken property and this mutation had any subscriptions (or subscribers) 25 | if ( 26 | key !== 'subResolvers' && 27 | key !== 'userToken' && 28 | traql[key].expectedNumberOfAqls >= 1 29 | ) { 30 | traql[key].mutationId = key; 31 | // if we receive all of the expected Aqls, aka have resolved this traql entry 32 | if ( 33 | traql[key].expectedNumberOfAqls === 34 | traql[key].aqlsReceivedBack.length 35 | ) { 36 | // send the traql back to the server so it can be stored in the DB 37 | aqlsToBeSent.successAqls.push(traql[key]); 38 | delete traql[key]; 39 | } else { 40 | // check if traql obj has "give me one more chance property" 41 | if (traql[key].probation) { 42 | // otherwise send successful aqls to server for entry into the db 43 | aqlsToBeSent.errorAqls.push(traql[key]); 44 | delete traql[key]; 45 | } else { 46 | traql[key].probation = true; 47 | } 48 | } 49 | } 50 | } 51 | postReq.data = aqlsToBeSent; 52 | axios(postReq) 53 | .then((res) => { 54 | console.log('successful addition of aqls to db'); 55 | }) 56 | .catch((err) => console.log('err')); 57 | } 58 | open = true; 59 | } 60 | } 61 | 62 | module.exports = traqlAudit; 63 | -------------------------------------------------------------------------------- /__tests__/server/newTraqlEntry.js: -------------------------------------------------------------------------------- 1 | const { expect, it, beforeEach, afterEach } = require('@jest/globals'); 2 | const newTraqlEntry = require('../../server/newTraqlEntry'); 3 | 4 | describe('create newTraqlEntry', () => { 5 | 6 | beforeEach(() => { 7 | traql = {subResolvers: 2}; 8 | args = {aql: {mutationId: 1, resolver: 'resolver', userToken: 2}}; 9 | pubsub = {subscriptions: {key1: 1, key2: 2, key3: 3, key4: 4}}; 10 | }); 11 | 12 | afterEach(() => { 13 | traql = {}; 14 | args = {}; 15 | pubsub = {}; 16 | }); 17 | 18 | it('should create a new attribute on the traql object with a key name of the mutation id and the value of an object', () => { 19 | expect(Object.keys(traql).length === 1); 20 | expect(traql.hasOwnProperty(1) === false); 21 | newTraqlEntry(traql, args, pubsub); 22 | expect(Object.keys(traql).length === 2); 23 | expect(traql.hasOwnProperty(1) === true); 24 | expect(typeof traql[1]).toBe('object'); 25 | }); 26 | 27 | it('should add to the created object a key named resolver with the value args.aql.resolver from the passed in args parameter', () => { 28 | newTraqlEntry(traql, args, pubsub); 29 | expect(traql[1].resolver).toEqual(args.aql.resolver); 30 | }); 31 | 32 | it('should have a non-null resolver attribute', () => { 33 | newTraqlEntry(traql, args, pubsub); 34 | expect(traql[1].resolver).not.toBeUndefined(); 35 | expect(traql[1].resolver).not.toBeNull(); 36 | }); 37 | 38 | it('should add to the created object a key named openedTime with a value of the current time', () => { 39 | const realDateNow = Date.now.bind(global.Date); 40 | const dateNowStub = jest.fn(() => 1530518207007); 41 | global.Date.now = dateNowStub; 42 | newTraqlEntry(traql, args, pubsub); 43 | expect(traql[1].openedTime).toBe(Date.now()); 44 | global.Date.now = realDateNow; 45 | }); 46 | 47 | it('should add to the created object a key named expectedNumberOfAqls with the values of the number of subscriptions divided by the number of resolvers', () => { 48 | newTraqlEntry(traql, args, pubsub); 49 | expect(traql[1].expectedNumberOfAqls).toBe(2); 50 | }); 51 | 52 | it('should have a non-null expectedNumberOfAqls attribute', () => { 53 | newTraqlEntry(traql, args, pubsub); 54 | expect(traql[1].expectedNumberOfAqls + 1).toBeTruthy(); 55 | }); 56 | 57 | it('should add to the created object a key named aqlsReceivedBack and a value of an empty array', () => { 58 | newTraqlEntry(traql, args, pubsub); 59 | expect(traql[1].aqlsReceivedBack).toEqual([]); 60 | }); 61 | 62 | it('should add to the created object a key named userToken and a value args.aql.userToken from the passed in args parameter', () => { 63 | newTraqlEntry(traql, args, pubsub); 64 | expect(traql[1].userToken).toBe(args.aql.userToken); 65 | }); 66 | 67 | it('should have a non-null userToken attribute', () => { 68 | newTraqlEntry(traql, args, pubsub); 69 | expect(traql[1].userToken).not.toBeUndefined(); 70 | expect(traql[1].userToken).not.toBeNull(); 71 | }); 72 | 73 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aqls - Server 2 | 3 | 4 | ## Overview 5 | GraphQL analytics toolkit and dashboard that integrate with Apollo Client and Apollo Server Express. It is an easy to use analytics suite that monitors GraphQL subscriptions concurrency, latency, errors, and resolver frequency. Our integrated dashboard displays your GraphQL analytics on dynamic and interactive charts. 6 | 7 | This package is for setting up your server. For the client-side package please refer to the [Aqls-client](https://github.com/oslabs-beta/Aqls-client) package. 8 | 9 | **Note:** Aqls is currently in BETA and improvements will continue to be implemented. If any issues are encountered while using our application, please submit a PR. 10 | 11 | 12 | ## Requirements 13 | - **Node.js** *version* 12.18.3+ 14 | - **apollo-server-express** *version* 2.18.1+ 15 | 16 | ## Install 17 | With npm: 18 | 19 | ``` 20 | npm install --save @aqls/server 21 | ``` 22 | 23 | ## Getting Started 24 | 25 | #### 26 | - [ ] **1**. Import Traql, traqlAudit, and analyticsRouter into your server file: 27 | 28 | ```javascript 29 | const { Traql, traqlAudit, analyticsRouter } = require('@aqls/server'); 30 | ``` 31 | #### 32 | 33 | - [ ] **2**. Initialize a constant for your resolvers: 34 | ```javascript 35 | const resolvers = { Mutation, Query, Subscription }; 36 | ``` 37 | #### 38 | - [ ] **3**. Create a new instance of Traql passing in the resolvers and your user token: 39 | ```javascript 40 | const traql = new Traql(resolvers, 'INSERT USER TOKEN HERE IN QUOTES'); 41 | ``` 42 | This will keep track of the number of subscription resolvers in the system and will calculate the number of current subscribers. You can get your User Token from [Aqls.io](https://www.aqls.io/) by signing up through Github OAuth. This token is needed to view your analytics in the developer dashboard at Aqls.io. 43 | #### 44 | - [ ] **4**. Add to your Apollo Server: 45 | ```javascript 46 | const server = new ApolloServer({ 47 | typeDefs, 48 | resolvers, 49 | context: { db, pubsub, traql }, 50 | }); 51 | ``` 52 | Make sure that *context* is added to your server. Be sure to pass in **BOTH** *pubsub* and *traql* to server context. 53 | #### 54 | - [ ] **5**. Invoke the traqlAudit function passing in traql: 55 | ```javascript 56 | setInterval(() => traqlAudit(traql), 5000); 57 | ``` 58 | traqlAudit is invoked every 5 seconds to audit each traql entry and ensure that there are no errors before sending the data to be displayed on the dashboard. 59 | #### 60 | - [ ] **6**. Instantiate an endpoint to send analytics to Aqls server: 61 | ```javascript 62 | app.use('/aqlsanalytics', analyticsRouter(traql)); 63 | ``` 64 | **Note:** Please ensure the endpoint is *'/aqlsanalytics'* and invoke *analyticsRouter* passing in *traql*. 65 | ### 66 | ### 67 | ## Update Schema and Mutation Tracking 68 | - [ ] **1**. Add AQL and AQLInput to schema file: 69 | ```graphQL 70 | type AQL { 71 | mutationSendTime: String, 72 | mutationReceived: String, 73 | subscriberReceived: String, 74 | mutationId: ID, 75 | resolver: String, 76 | userToken: String, 77 | } 78 | input AQLInput { 79 | mutationSendTime: String, 80 | mutationReceived: String, 81 | subscriberReceived: String, 82 | mutationId: ID, 83 | resolver: String, 84 | userToken: String, 85 | } 86 | ``` 87 | ### 88 | - [ ] **2**. Add AQLInput type to any mutation type that you want to track: 89 | ```graphQL 90 | type Mutation { 91 | newColor(colorArg: String, aql: AQLInput): Color 92 | newLuckyNumber(numberArg: Int, aql: AQLInput): LuckyNumber 93 | } 94 | ``` 95 | ### 96 | - [ ] **3**. Add AQL type to any payload that you want to track: 97 | ```graphQL 98 | type UpdatedColorPayload { 99 | cssColor: String! 100 | id: ID! 101 | aql: AQL 102 | } 103 | ``` 104 | ### 105 | ### 106 | ### Mutation Tracking 107 | - [ ] **1**. Import newAqlPayload in mutation resolver file: 108 | ```javascript 109 | const { newAqlPayload } = require('@aqls/server'); 110 | ``` 111 | - [ ] **2**. Pass invocation of newAqlPayload to pubsub publish property: 112 | ```javascript 113 | function newColor(parent, args, { db, pubsub, traql }, info) { 114 | db.color.cssColor = args.colorArg; 115 | const payload = { 116 | updatedColor: { 117 | ...db.color, 118 | }, 119 | }; 120 | pubsub.publish('COLOR_MUTATED', newAqlPayload(payload, args, traql, pubsub)); 121 | return db.color; 122 | } 123 | ``` 124 | **Note:** Pass in payload as first argument, be sure to include resolver arguments and the traql and pubsub from server context. 125 | #### 126 | #### 127 | - [ ] **Lastly, Connect with the Aqls Team!** 128 | 129 | aqlorgteam@gmail.com 130 | 131 | Case Simmons: [Case's Github](https://github.com/casesimmons) and [Case's LinkedIn](https://www.linkedin.com/in/case-simmons/) 132 | 133 | Julie Pinchak: [Julie's Github](https://github.com/jpinchak) and [Julie's LinkedIn](https://www.linkedin.com/in/julie-pinchak/) 134 | 135 | Michael O'Halloran: [Michael's Github](https://github.com/LordRegis22) and [Michael's LinkedIn](https://www.linkedin.com/) 136 | 137 | Rocio Infante: [Rocio's Github](https://github.com/Rocio-Infante) and [Rocio's LinkedIn](https://www.linkedin.com/in/rocio-infante/) 138 | --------------------------------------------------------------------------------