├── .DS_Store ├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── client │ ├── App.test.tsx │ ├── api │ │ └── apiFetch.test.tsx │ └── components │ │ ├── Demo.test.tsx │ │ ├── Nav.test.tsx │ │ └── QueryResult.test.tsx └── setup.ts ├── client ├── App.css ├── App.tsx ├── api │ ├── apiFetch.tsx │ └── clearCache.tsx ├── assets │ ├── clock.png │ ├── copyIcon.png │ ├── dan.png │ ├── dashQL_Logo.png │ ├── drew.png │ ├── fonts │ │ ├── Khula-Bold.ttf │ │ ├── Khula-ExtraBold.ttf │ │ ├── Khula-Light.ttf │ │ ├── Khula-Regular.ttf │ │ ├── Khula-SemiBold.ttf │ │ └── OFL.txt │ ├── fronheiser.jpeg │ ├── github-logo.png │ ├── hands.png │ ├── kevin.jpg │ ├── linkedin-logo.png │ └── save.png ├── components │ ├── Contact.tsx │ ├── Demo.tsx │ ├── Demo_Components │ │ ├── BarChart.tsx │ │ ├── LineChart.tsx │ │ ├── PieChart.tsx │ │ ├── QueryResult.tsx │ │ ├── ResultCard.tsx │ │ └── queryCode.tsx │ ├── Docs.tsx │ ├── Home.tsx │ ├── Nav.tsx │ └── styles │ │ ├── Contact.css │ │ ├── Demo.css │ │ └── Home.css ├── index.css ├── main.tsx └── vite-env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public └── vite.svg ├── server ├── .env ├── Schemas │ └── schema.ts ├── api │ └── index.ts ├── dashQL │ ├── dashCache.ts │ └── queryHandler.ts ├── models │ └── model.ts ├── public │ └── .gitkeep ├── redis.ts ├── server.ts ├── types │ └── types.ts └── vercel.json ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dashQL/61efbc4935be41040bd06ae9344708d1f0cd97a1/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 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 |
2 | 3 | ![dashQL_Logo](https://github.com/oslabs-beta/dashQL/assets/129707410/711f1cc0-6076-4a83-8c27-c70e22a665a9) 4 | 5 | 6 | ![graphql](https://img.shields.io/badge/GraphQl-E10098?style=for-the-badge&logo=graphql&logoColor=white) 7 | ![redis](https://img.shields.io/badge/redis-%23DD0031.svg?&style=for-the-badge&logo=redis&logoColor=white) 8 | ![typescript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 9 | ![vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 10 | 11 | 12 | ![react](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) 13 | ![chartjs](https://img.shields.io/badge/Chart%20js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white) 14 | ![node](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white) 15 | ![postgres](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white) 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | # About 26 | 27 | dashQL is a lightweight GraphQL caching tool utilizing Redis to cache queries at the field level to further improve reponse time and decrease the number of redundant requests required 28 | 29 | ## Table of Contents 30 | 1. [Features](https://github.com/oslabs-beta/dashQL/edit/main/README.md#features) 31 | 2. [Getting Started](https://github.com/oslabs-beta/dashQL/edit/main/README.md#getting-started) 32 | 3. [Demo](https://github.com/oslabs-beta/dashQL/edit/main/README.md#demo) 33 | 4. [Contributing](https://github.com/oslabs-beta/dashQL/edit/main/README.md#contributing-to-dashql) 34 | 5. [Authors](https://github.com/oslabs-beta/dashQL/edit/main/README.md#authors) 35 | 36 | # Features 37 | * Allows for caching at the field level from the GraphQL abstract syntax tree 38 | * Ability to handle query requests both with and without an argument 39 | * Ability to cache deeply nested queries 40 | 41 | 42 | # Getting Started 43 | 1. Install redis and define host and port in server file 44 | ```Javascript 45 | const redisClient = redis.createClient({ 46 | host: "localhost", 47 | port: 6379, 48 | }); 49 | ``` 50 | 2. Download dashQL as an npm module and save it to your package.json as a dependency 51 | ```Javascript 52 | npm install dashql 53 | ``` 54 | 3. Import dashQL into server file 55 | ```Javascript 56 | const dashQL = require('dashQL'); 57 | ``` 58 | 4. Create an instance of dashQL by passing in redisClient in server file 59 | 60 | ```Javascript 61 | const dashQL = new dashQL(redisClient); 62 | ``` 63 | 64 | 5. On your server file which handles graphQL requests, name the endpoint '/dashQL' and put in dashQL as your middleware and return res.locals back to client 65 | 66 | ```Javascript 67 | app.use('/dashQL', dashQL, (req: Request, res: Response) => { 68 | return res.status(200).send(res.locals); 69 | }); 70 | ``` 71 | 72 | 6. Enjoy significantly faster response times specifically at the field level not just per query string! 73 | 74 | 75 | 76 | # Demo 77 | Feel free to visit our website to get an interactive demonstration of how our caching tool works and view the significantly improved response times 78 | 79 | After entering our site, you will be met with a demonstration with the ability to run GraphQL queries with an interactive selection tool utilizing the Star Wars API. 80 | 81 | 1. Select the fields you would like to query and a preview of the GraphQL query will be shown 82 | 83 | ![name mass](https://github.com/oslabs-beta/dashQL/assets/129707410/0ec087f5-788f-4fc3-8c71-91043b2299ef) 84 | 85 | 2. Click the 'Run Query' button to see the GraphQL query result. The metrics above will show the uncached response time populated on the graph and a cache hit/miss result will be logged to the bar charts. A cache miss will be logged the first time a unique query is run indicating that the query was not found in our cache and will be stored. 86 | 87 | ![name-mass-time](https://github.com/oslabs-beta/dashQL/assets/129707410/41d99ef2-2a4c-475a-ac4f-1d85b4eaa0eb) 88 | 89 | 90 | 3. This time, select an additional field to query. Then click the 'Run Query' button. The response time has drastically decreased as the previous field level queries were stored in the cache and the server only has to go to the database for one field. 91 | 92 | ![name-mass-eye color](https://github.com/oslabs-beta/dashQL/assets/129707410/da64d56d-ecde-4d2b-81b3-13181ca7c1b7) 93 | 94 | 4. Feel free to play around with nested queries by selecting 'Species' or queries without an '_id' argument. 95 | 96 | ![nested](https://github.com/oslabs-beta/dashQL/assets/129707410/5fbbf576-308a-4d01-afc1-3bcf99af4177) 97 | In this last example, you will notice the response time is decreased even further on the third query request as now all information is coming from the cache. 98 | 99 | 5. Lastly, click the 'Clear Cache' button to clear the cache and start over. 100 | 101 | 102 | 103 | # Contributing to dashQL 104 | The team at dashQL is excited you are interested in contributing to dashQL. 105 | 1. Fork repo from the `dev` branch 106 | 2. Create your feature branch (`git checkout -b feature/yourFeature`) 107 | 3. Stage and commit your changes (`git commit -m "added cool new feature"`) 108 | 4. Push to branch (`git push origin feature/yourFeature`) 109 | 5. Open a pull request to `dev` which will be reviewed by one of the dashQL authors prior to merge 110 | 111 | ## Roadmap for Future Development 112 | - [ ] Handle mutations 113 | - [ ] End-to-end testing 114 | - [ ] Additional eviction strategies 115 | - [ ] Security features 116 | 117 | # Authors 118 | Dan Bonney [LinkedIn](https://www.linkedin.com/in/dan-bonney/) | [Github](https://github.com/D-Bonney) 119 | Meredith Fronheiser [LinkedIn](https://www.linkedin.com/in/meredith-fronheiser/) | [Github](https://github.com/mfronheiser) 120 | Kevin Klochan [LinkedIn](https://www.linkedin.com/in/kevin-klochan-7a0ba7218/) | [Github](https://github.com/kevinklochan) 121 | Drew Williams [LinkedIn](https://www.linkedin.com/in/andrew-vaughan-williams/) | [Github](https://github.com/avwilliams1995) 122 | 123 | 124 | 125 | Please ⭐ our repo if you've found this useful, we want to be able to help as many of developers as we can! 126 | -------------------------------------------------------------------------------- /__tests__/client/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import { MemoryRouter } from "react-router-dom"; 3 | 4 | import App from "../../client/App"; 5 | import "react-router-dom"; 6 | 7 | describe("App", () => { 8 | 9 | test("renders App", () => { 10 | render( 11 | 12 | 13 | 14 | ); 15 | 16 | screen.debug(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/client/api/apiFetch.test.tsx: -------------------------------------------------------------------------------- 1 | import getData from "../../../client/api/apiFetch"; 2 | 3 | // mock for being able to render nav page 4 | 5 | 6 | describe("API Fetch Test", () => { 7 | const result: any = { 8 | peopleRes: null, 9 | planetsRes: null, 10 | nestedRes: null, 11 | peopleNoId: null, 12 | planetsNoId: null, 13 | }; 14 | beforeEach(async () => { 15 | const pplStr = "query {people (_id:1){name, mass, , , }}"; 16 | result.peopleRes = await getData({ query: pplStr }); 17 | const planetStr = "query {planets (_id:1){name, population, , , }}"; 18 | result.planetsRes = await getData({ query: planetStr }); 19 | const nestedStr = "query {people (_id:1){name, , , species { name, } }}"; 20 | result.nestedRes = await getData({ query: nestedStr }); 21 | const peopleNoIdStr = "{peopleNoId {name, , , , }}"; 22 | result.peopleNoId = await getData({ query: peopleNoIdStr }); 23 | const planetsNoIdStr = "{planetsNoId {name, , , , }}"; 24 | result.planetsNoId = await getData({ query: planetsNoIdStr }); 25 | }); 26 | test("Testing fetch to /dashQl endpoint for people schema with id", async () => { 27 | // tests 28 | expect(result.peopleRes.res).toStrictEqual( 29 | '{"data":{"people":{"name":"Luke Skywalker","mass":77}}}' 30 | ); 31 | expect(result.peopleRes).toHaveProperty("res"); 32 | expect(result.peopleRes).toHaveProperty("cacheHit"); 33 | expect(result.peopleRes).toHaveProperty("hitPercentage"); 34 | expect(result.peopleRes).toHaveProperty("missPercentage"); 35 | expect(result.peopleRes).toHaveProperty("time"); 36 | expect(result.peopleRes).toHaveProperty("totalHits"); 37 | expect(result.peopleRes).toHaveProperty("totalQueries"); 38 | }); 39 | 40 | test("Testing fetch to /dashQl endpoint for planets schema with id", async () => { 41 | // tests 42 | expect(result.planetsRes.res).toStrictEqual( 43 | '{"data":{"planets":{"name":"Tatooine","population":200000}}}' 44 | ); 45 | expect(result.planetsRes).toHaveProperty("res"); 46 | expect(result.planetsRes).toHaveProperty("cacheHit"); 47 | expect(result.planetsRes).toHaveProperty("hitPercentage"); 48 | expect(result.planetsRes).toHaveProperty("missPercentage"); 49 | expect(result.planetsRes).toHaveProperty("time"); 50 | expect(result.planetsRes).toHaveProperty("totalHits"); 51 | expect(result.planetsRes).toHaveProperty("totalQueries"); 52 | }); 53 | 54 | test("Testing fetch to /dashQl endpoint for nested people schema with id", async () => { 55 | // tests 56 | expect(result.nestedRes.res).toStrictEqual( 57 | '{"data":{"people":{"name":"Luke Skywalker","species":{"name":"Human"}}}}' 58 | ); 59 | expect(result.nestedRes).toHaveProperty("res"); 60 | expect(result.nestedRes).toHaveProperty("cacheHit"); 61 | expect(result.nestedRes).toHaveProperty("hitPercentage"); 62 | expect(result.nestedRes).toHaveProperty("missPercentage"); 63 | expect(result.nestedRes).toHaveProperty("time"); 64 | expect(result.nestedRes).toHaveProperty("totalHits"); 65 | expect(result.nestedRes).toHaveProperty("totalQueries"); 66 | }); 67 | 68 | test("Testing fetch to /dashQl endpoint for people schema with NO id", async () => { 69 | // tests 70 | const parsedRes = await JSON.parse(result.peopleNoId.res); 71 | const expectedRes = []; 72 | for (let dataPoint of parsedRes.data.peopleNoId.name) { 73 | if (expectedRes.length >= 10) { 74 | break; 75 | } 76 | expectedRes.push(dataPoint); 77 | } 78 | 79 | expect(expectedRes).toStrictEqual([ 80 | "Luke Skywalker", 81 | "C-3PO", 82 | "R2-D2", 83 | "Darth Vader", 84 | "Leia Organa", 85 | "Owen Lars", 86 | "Beru Whitesun lars", 87 | "R5-D4", 88 | "Biggs Darklighter", 89 | "Obi-Wan Kenobi", 90 | ]); 91 | expect(result.peopleNoId).toHaveProperty("res"); 92 | expect(result.peopleNoId).toHaveProperty("cacheHit"); 93 | expect(result.peopleNoId).toHaveProperty("hitPercentage"); 94 | expect(result.peopleNoId).toHaveProperty("missPercentage"); 95 | expect(result.peopleNoId).toHaveProperty("time"); 96 | expect(result.peopleNoId).toHaveProperty("totalHits"); 97 | expect(result.peopleNoId).toHaveProperty("totalQueries"); 98 | }); 99 | 100 | test("Testing fetch to /dashQl endpoint for planets schema with NO id", async () => { 101 | const parsedRes = await JSON.parse(result.planetsNoId.res); 102 | const expectedRes = []; 103 | for (let dataPoint of parsedRes.data.planetsNoId.name) { 104 | if (expectedRes.length >= 10) { 105 | break; 106 | } 107 | expectedRes.push(dataPoint); 108 | } 109 | 110 | expect(expectedRes).toStrictEqual([ 111 | "Alderaan", 112 | "Yavin IV", 113 | "Hoth", 114 | "Dagobah", 115 | "Bespin", 116 | "Endor", 117 | "Naboo", 118 | "Coruscant", 119 | "Kamino", 120 | "Geonosis", 121 | ]); 122 | expect(result.planetsNoId).toHaveProperty("res"); 123 | expect(result.planetsNoId).toHaveProperty("cacheHit"); 124 | expect(result.planetsNoId).toHaveProperty("hitPercentage"); 125 | expect(result.planetsNoId).toHaveProperty("missPercentage"); 126 | expect(result.planetsNoId).toHaveProperty("time"); 127 | expect(result.planetsNoId).toHaveProperty("totalHits"); 128 | expect(result.planetsNoId).toHaveProperty("totalQueries"); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /__tests__/client/components/Demo.test.tsx: -------------------------------------------------------------------------------- 1 | // import { render, screen } from "@testing-library/react"; 2 | // import Demo from "../../../client/components/Demo"; 3 | // import { MemoryRouter } from "react-router-dom"; 4 | // import { beforeEach } from "vitest"; 5 | // import { useState } from "react"; 6 | 7 | // let currentPage = "Demo"; 8 | // function changePage(page: string): void { 9 | // currentPage = page; 10 | // } 11 | 12 | // describe("Demo Test", () => { 13 | // beforeEach(async () => { 14 | // render(); 15 | // screen.debug() 16 | // }); 17 | 18 | // test("renders Demo component", () => { 19 | // expect(screen.getByText("dashQL Cache Demo")).toBeInTheDocument(); 20 | // }); 21 | // }); 22 | -------------------------------------------------------------------------------- /__tests__/client/components/Nav.test.tsx: -------------------------------------------------------------------------------- 1 | // import { render, screen } from '@testing-library/react'; 2 | // import Nav from '../../../client/components/Nav'; 3 | // import { MemoryRouter } from 'react-router-dom'; 4 | // import { beforeEach } from 'vitest'; 5 | 6 | // // mock for being able to render nav page 7 | // const currentPage:string = "Home" 8 | 9 | // describe('Navbar Test', () => { 10 | // beforeEach(async () => { 11 | // render( 12 | // 13 | //