├── .DS_Store ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── cacheiql-client ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── cacheManagement.ts │ ├── errorhandling.ts │ ├── export.ts │ ├── generatekey.ts │ ├── index.ts │ ├── mutationHandler.ts │ └── types.ts └── tsconfig.json ├── cacheiql-server ├── .DS_Store ├── README.md ├── jest.config.js ├── package-lock 2.json ├── package-lock.json ├── package.json ├── src │ ├── .DS_Store │ ├── cache │ │ ├── .DS_Store │ │ ├── cacheManager.ts │ │ ├── cacheUtils.ts │ │ └── redisClient.ts │ ├── cli │ │ ├── generateSchema.ts │ │ └── index.ts │ ├── index.ts │ ├── middleware │ │ └── cacheMiddleware.ts │ ├── query │ │ ├── mergeUtils.ts │ │ ├── partialResolver.ts │ │ └── queryParser.ts │ └── schema │ │ ├── introspection.ts │ │ ├── schemaLoader.ts │ │ └── validation.ts ├── tests │ ├── cacheManager.test.js │ ├── cacheManager.test.ts │ ├── index.test.js │ ├── index.test.ts │ ├── redisClient.test.js │ └── redisClient.test.ts └── tsconfig.json ├── test-client ├── distTSC │ ├── client │ │ ├── components │ │ │ ├── CharacterCard.js │ │ │ ├── Dashboard.js │ │ │ ├── HitMiss.js │ │ │ └── ReviewCard.js │ │ ├── index.js │ │ └── types.js │ ├── server │ │ ├── models │ │ │ └── starWarsModels.js │ │ ├── schema │ │ │ ├── resolvers.js │ │ │ └── schema.js │ │ └── server.js │ └── test-client │ │ └── src │ │ ├── client │ │ ├── components │ │ │ ├── CharacterCard.js │ │ │ ├── Dashboard.js │ │ │ ├── HitMiss.js │ │ │ └── ReviewCard.js │ │ ├── index.js │ │ └── types.js │ │ └── server │ │ ├── models │ │ └── starWarsModels.js │ │ ├── schema │ │ ├── resolvers.js │ │ └── schema.js │ │ └── server.js ├── package-lock.json ├── package.json ├── src │ ├── client │ │ ├── components │ │ │ ├── CharacterCard.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── HitMiss.tsx │ │ │ └── ReviewCard.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ ├── styles │ │ │ ├── characterCard.scss │ │ │ ├── dashboard.scss │ │ │ └── time.scss │ │ └── types.ts │ └── server │ │ ├── models │ │ └── starWarsModels.ts │ │ ├── schema │ │ ├── resolvers.ts │ │ └── schema.ts │ │ └── server.ts ├── tsconfig.json └── webpack.config.js └── test-server ├── .DS_Store ├── distTSC ├── src │ ├── client │ │ └── index.js │ └── server │ │ ├── models │ │ └── starWarsModels.js │ │ ├── schema │ │ ├── resolvers.js │ │ └── schema.js │ │ └── server.js └── webpack.config.js ├── dump.rdb ├── package-lock.json ├── package.json ├── src ├── .DS_Store ├── client │ ├── index.html │ └── index.tsx └── server │ ├── .DS_Store │ ├── models │ └── starWarsModels.ts │ ├── schema │ ├── resolvers.ts │ └── schema.ts │ └── server.ts ├── tsconfig.json └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | .vscode/ 4 | dist/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/src/index.ts", 13 | // "preLaunchTask": "tsc: build - tsconfig.json", 14 | "outFiles": ["${workspaceFolder}/**/*.js"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Screenshot 2024-11-26 at 6 14 03 PM 2 | 3 | # CacheIQL 4 | 5 | 6 | cacheiql is an efficient JavaScript library designed to optimize GraphQL performance through intelligent caching, 7 | effectively minimizing redundant API requests resulting in heavily reduced response time. 8 | With our easy-to-use implementation, 9 | CacheIQL provides the developer with enhanced application speed and efficiency through a seamless experience. 10 | 11 | ## Installation 12 | 13 | **CacheIQL Client & CacheIQL Server** 14 | 15 | CacheIQL is split into two npm packages: 16 | 17 | -Install [cacheiql/client](https://www.npmjs.com/package/cacheiql-client) from npm using the terminal command `npm i cacheiql-client`
18 | -Install [cacheiql/server](https://www.npmjs.com/package/cacheiql-server) from npm using the terminal command `npm i cacheiql-server`
19 | 20 | ## Features 21 | 22 | 23 | * server-side caching through implementation of a local Redis instance
24 | * client-side caching through implementation of localStorage
25 | * effective mutation handling through cache invalidation
26 | * custom fetch and caching 'CacheIt' function
27 | * implementation of request deduplication through the use of caching initial user query response objects
28 | * code maintainability through integration of TypeScript with strict typing, effectively reducing runtime errors
29 | * iteration upon GraphQL's field-level fetching as well as query batching to optimize speed upon initial query retrieval
30 | 31 | ## Documentation 32 | 33 | 34 | [CLIENT README](https://github.com/oslabs-beta/CacheIQL/blob/production/cacheiql-client/README.md)
35 | [SERVER README](https://github.com/oslabs-beta/CacheIQL/blob/production/cacheiql-server/README.md) 36 | 37 | Thank you for your interest in our tool!
38 | -CacheIQL Team 39 | 40 | ## Contributors 41 | 42 | 43 | Through the help of [OS Labs](https://github.com/oslabs-beta) , developed by [Vasean Annin](https://github.com/VaseanAnnin), [Gabriella Davoudpour 44 | ](https://github.com/gabyd613), [George German](https://github.com/GeorgeGerman29), [Pedram Kashani ](https://github.com/PedramKashani), and [Chris Matzen](https://github.com/matzec42) 45 | -------------------------------------------------------------------------------- /cacheiql-client/README.md: -------------------------------------------------------------------------------- 1 | # @cacheiql/client 2 | 3 | '@cacheiql/client is a lightweight client-side caching solution for GraphQL queries, designed to improve response times through local storage and request deduplication. By intercepting GraphQL requests, CacheIQL checks the cache for stored responses and only fetches missing data from the server, minimizing redundant network requests. Its efficient key-value storage approach enables quick retrieval and cache updates while ensuring data consistency. CacheIQL seamlessly integrates with existing GraphQL clients, optimizing performance without requiring additional backend configuration. 4 | 5 | @cacheiql/client is an npm package powered through [OS Labs](https://github.com/oslabs-beta) , developed by [Vasean Annin](https://github.com/VaseanAnnin), [Gabriella Davoudpour 6 | ](https://github.com/gabyd613), [George German](https://github.com/GeorgeGerman29), [Pedram Kashani ](https://github.com/PedramKashani), and [Chris Matzen](https://github.com/matzec42). 7 | 8 | ## Installation 9 | 10 | Install [cacheiql/client](https://www.npmjs.com/package/cacheiql-client) from npm using the terminal command: `npm i cacheiql-client`
11 | @cacheiql/client will be added as a dependency to your package.json file. 12 | 13 | ## Import CacheIQL-Server 14 | 15 | **1. Add the following import to your .jsx file:** 16 | 17 | CommonJS: 18 | ``` js 19 | const { cacheIt } = require('cacheiql-client'); 20 | ``` 21 | ES6+: 22 | ``` js 23 | import { cacheIt } from 'cacheiql-client'; 24 | ``` 25 | 26 | **2. Pass into the cacheIt function your desired endpoint, query, and TTL (time-to-live)** 27 | 28 | ``` js 29 | const responseCharacter: ResponseObject = await cacheIt({ 30 | endpoint: 'http://localhost:3000/graphql', 31 | query: ` 32 | { 33 | people{ 34 | _id 35 | gender 36 | birth_year 37 | skin_color 38 | hair_color 39 | name 40 | species_id 41 | homeworld_id 42 | } 43 | }`, 44 | time: 3600, 45 | }); 46 | ``` 47 | **3. Start the server by running the necessary commands in your terminal:**

48 | ex. `npm run start` | `npm run serv` 49 | 50 |
51 | 52 | **4. Visit http://localhost:3000/graphql to access GraphQL and start testing the caching functionality.** 53 | 54 | 55 | ## How It Works 56 | 57 | Screenshot 2025-02-25 at 6 47 03 PM 58 | 59 | **With Queries:** 60 | 61 | CacheIQL's client side functionality begins with the user invoking the custom 'cacheIt' fetch and caching function, passing in the query. CacheIQL begins by interpreting the query, and checking for its existence in the cache. If it is not found, CacheIQL makes the query to GraphQL, storing the query: response object as a key value pair in local storage. If the query is found in local storage, the prior step is omitted, and cacheiql grabs the response object associated with the query, returning it back to the user with an increased latency of over 1000%. 62 | 63 | **Image A**
64 | ![screenshot_2025-02-25_at_17 37 50_360](https://github.com/user-attachments/assets/23bba352-7865-4696-82cb-46fd1ab28b10)

65 | **Image B**
66 | ![screenshot_2025-02-25_at_17 38 28](https://github.com/user-attachments/assets/7e8bd6c2-90d5-4abe-b29b-19144cc82e6f) 67 | 68 | Here, we can see the difference in fetching speeds with the use of the native fetch API (image A) vs. CacheIQL's caching feature (image B), an increase in response time from 728 ms to .8 ms. 69 | 70 | **With Mutations:** 71 | 72 | Through the use of introspection, cacheiql is able to extract the query name off of the AST to determine whether or not a mutation keyword is used. If so, cacheiql implements cache invalidation, removing the initial query: response object associated with the mutation within miliseconds, allowing the user to re-update the cache with the most up to date form of data avaliable. 73 | -------------------------------------------------------------------------------- /cacheiql-client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheiql-client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "cacheiql-client", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "graphql": "^16.9.0", 13 | "graphql-tag": "^2.12.6", 14 | "tsc": "^2.0.4", 15 | "typescript": "^5.7.2" 16 | } 17 | }, 18 | "node_modules/graphql": { 19 | "version": "16.9.0", 20 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", 21 | "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", 22 | "license": "MIT", 23 | "engines": { 24 | "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" 25 | } 26 | }, 27 | "node_modules/graphql-tag": { 28 | "version": "2.12.6", 29 | "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", 30 | "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", 31 | "license": "MIT", 32 | "dependencies": { 33 | "tslib": "^2.1.0" 34 | }, 35 | "engines": { 36 | "node": ">=10" 37 | }, 38 | "peerDependencies": { 39 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" 40 | } 41 | }, 42 | "node_modules/tsc": { 43 | "version": "2.0.4", 44 | "resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz", 45 | "integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==", 46 | "license": "MIT", 47 | "bin": { 48 | "tsc": "bin/tsc" 49 | } 50 | }, 51 | "node_modules/tslib": { 52 | "version": "2.8.1", 53 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 54 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 55 | "license": "0BSD" 56 | }, 57 | "node_modules/typescript": { 58 | "version": "5.7.2", 59 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 60 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 61 | "license": "Apache-2.0", 62 | "bin": { 63 | "tsc": "bin/tsc", 64 | "tsserver": "bin/tsserver" 65 | }, 66 | "engines": { 67 | "node": ">=14.17" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cacheiql-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheiql-client", 3 | "version": "1.0.0", 4 | "description": "client side graphQL tool", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "watch": "tsc --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/oslabs-beta/CacheIQL.git" 14 | }, 15 | "keywords": [ 16 | "client", 17 | "graphQL", 18 | "OSP" 19 | ], 20 | "author": "Vasean Annin, George German, Gabriella Davoudpour, Pedram Kashani, Chris Matzen", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/oslabs-beta/CacheIQL/issues" 24 | }, 25 | "homepage": "https://github.com/oslabs-beta/CacheIQL#readme", 26 | "dependencies": { 27 | "graphql": "^16.9.0", 28 | "graphql-tag": "^2.12.6", 29 | "tsc": "^2.0.4", 30 | "typescript": "^5.7.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cacheiql-client/src/cacheManagement.ts: -------------------------------------------------------------------------------- 1 | import { Query, Mutation } from './types'; 2 | import { generateKey } from './generatekey'; 3 | 4 | 5 | // cacheManager --- function for time-based cache management (removes cached data from local storage) 6 | export const cacheManager = (key: Query | Mutation, time: number = 60) => { 7 | 8 | // the amount of time passed in is how long the cache will stay within local storage; 9 | setTimeout(() => { 10 | const query = key; 11 | for (let i = 0; i < localStorage.length; i++) { 12 | if (localStorage.key(i) === query) { 13 | localStorage.removeItem(query); 14 | return; 15 | } 16 | } 17 | }, time * 1000); 18 | }; 19 | 20 | // checkAndSaveToCache --- function that handles saving data to local storage 21 | export const checkAndSaveToCache = ( 22 | query: Query, 23 | response?: object, 24 | variables?: object 25 | ): string | void | boolean | object => { 26 | if (query === null) { 27 | return 'query is null'; 28 | } 29 | const queryString = query; 30 | 31 | // function to create key for caching (key will be query string, value is response) 32 | const key = generateKey(queryString, variables); 33 | 34 | // consider checking mutation vs query before checking storage etc 35 | const data = localStorage.getItem(queryString); 36 | 37 | if (data) { 38 | return true; 39 | } 40 | // if there was no data previously stored in cache, 41 | // add the query and the response to local storage 42 | else if (!data && response) { 43 | // invoke generate key to find the data type name to store as key 44 | // //generateKey(data); 45 | localStorage.setItem(queryString, JSON.stringify(response)); 46 | return true; 47 | } 48 | // potentially add another else if to check if type is a mutation 49 | // if so, invoke mutation updater function 50 | else { 51 | return false; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /cacheiql-client/src/errorhandling.ts: -------------------------------------------------------------------------------- 1 | import { ClientErrorType } from './types'; 2 | 3 | // client error function// 4 | export const createClientError = (message: string): ClientErrorType => { 5 | return { 6 | log: message, 7 | status: 400, 8 | message: { err: 'Something went wrong in cacheiqlIt fetch' }, 9 | }; 10 | }; -------------------------------------------------------------------------------- /cacheiql-client/src/export.ts: -------------------------------------------------------------------------------- 1 | import { cacheiqItType } from "./types"; 2 | import { createClientError } from "./errorhandling"; 3 | import { checkAndSaveToCache, cacheManager } from "./cacheManagement"; 4 | import { matchMQ } from "./mutationHandler"; 5 | import { grabQueryName } from "./mutationHandler"; 6 | 7 | // cacheiqIt --- function that makes fetch 8 | export const cacheiqIt = async ({ 9 | endpoint, 10 | query, 11 | mutation, 12 | time, 13 | }: cacheiqItType): Promise => { 14 | if (query) { 15 | if (typeof query !== "string") { 16 | console.error( 17 | createClientError( 18 | "Query passed in is invalid. Please check to make sure its a string" 19 | ) 20 | ); 21 | } 22 | 23 | // logic for querying DB for uncached queries, retrieving cached queries & responses from localStorage 24 | if (query !== null) { 25 | try { 26 | const queryname: string = grabQueryName(query); 27 | // if query is not cached, make fetch to DB 28 | if (!checkAndSaveToCache(queryname) && typeof query === "string") { 29 | const response: Promise = await fetch(endpoint, { 30 | method: "POST", 31 | headers: { 32 | // need to change this later to account for variables 33 | "Content-Type": "application/json", 34 | }, 35 | body: JSON.stringify({ query: `query${query}` }), 36 | }) 37 | .then((res) => res.json()) 38 | .then((data) => { 39 | // error handling for if data contains an error 40 | if (data.errors) { 41 | console.error(data.errors[0]); 42 | return; 43 | } 44 | // cache newly fetched data 45 | checkAndSaveToCache(queryname, data); 46 | cacheManager(queryname, time); 47 | return data; 48 | }); 49 | return response; 50 | } else { 51 | // instead of storing the error object, this returns early with the error 52 | // reassurance operator ! 53 | if (JSON.parse(localStorage.getItem(queryname)!).errors) { 54 | console.error( 55 | JSON.parse(localStorage.getItem(queryname)!).errors[0] 56 | ); 57 | return; 58 | } 59 | const response: object = JSON.parse(localStorage.getItem(queryname)!); 60 | return response; 61 | } 62 | } catch (err) { 63 | if (err instanceof Error) { 64 | console.log( 65 | `${err}, Something wrong with fetching query through GraphQL!` 66 | ); 67 | return createClientError(err.message); 68 | } 69 | } 70 | } 71 | } 72 | 73 | if (mutation) { 74 | if (typeof mutation !== "string") { 75 | console.error( 76 | createClientError( 77 | "Mutation passed in is invalid. Please check to make sure its a string." 78 | ) 79 | ); 80 | } 81 | 82 | if (mutation !== null) { 83 | try { 84 | // if query is not cached, make fetch to DB 85 | if (!checkAndSaveToCache(mutation) && typeof mutation === "string") { 86 | const response: Promise = await fetch(endpoint, { 87 | method: "POST", 88 | headers: { 89 | // need to change this later to account for variables 90 | "Content-Type": "application/json", 91 | }, 92 | body: JSON.stringify({ query: `mutation${mutation}` }), 93 | }) 94 | .then((res) => res.json()) 95 | .then((data) => { 96 | // error handling for if data contains an error 97 | if (data.errors) { 98 | console.error(data.errors[0]); 99 | return; 100 | } 101 | matchMQ(endpoint); 102 | console.log('matchMQ func:', matchMQ(endpoint)); 103 | cacheManager(mutation, time); 104 | return data; 105 | }); 106 | return response; 107 | } else { 108 | // variable to hold query string (either pulled from object or as is) 109 | const mutationString = mutation; 110 | // instead of storing the error object, this returns early with the error 111 | // reassurance operator ! 112 | if (JSON.parse(localStorage.getItem(mutationString)!).errors) { 113 | console.error( 114 | JSON.parse(localStorage.getItem(mutationString)!).errors[0] 115 | ); 116 | return; 117 | } 118 | const response: object = JSON.parse( 119 | localStorage.getItem(mutationString)! 120 | ); 121 | return response; 122 | } 123 | } catch (err) { 124 | if (err instanceof Error) { 125 | console.log( 126 | `${err}, Something wrong with fetching query through GraphQL!` 127 | ); 128 | return createClientError(err.message); 129 | } 130 | } 131 | } 132 | } 133 | }; 134 | 135 | -------------------------------------------------------------------------------- /cacheiql-client/src/generatekey.ts: -------------------------------------------------------------------------------- 1 | import { Query } from './types'; 2 | 3 | 4 | // Helper Function 5 | // creates unique key for each query and response 6 | export const generateKey = (query: Query, variables?: object): string => { 7 | // iterate through passed in data to find first instance of data type name to store as key 8 | return `${query}_${JSON.stringify(variables)}`; 9 | }; 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /cacheiql-client/src/index.ts: -------------------------------------------------------------------------------- 1 | // Entry point for client-side caching 2 | import { cacheiqIt } from './export'; 3 | import { cacheiqItType, queryArray, mutationArray } from './types'; 4 | 5 | export const cacheIt = ({ 6 | endpoint, 7 | query, 8 | mutation, 9 | time 10 | }:cacheiqItType): any => { 11 | return cacheiqIt({endpoint, query, mutation, time}); 12 | }; 13 | -------------------------------------------------------------------------------- /cacheiql-client/src/mutationHandler.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import { queryArray, mutationArray } from './types'; 3 | import { visit } from 'graphql'; 4 | import { DocumentNode } from 'graphql'; 5 | import { createClientError } from './errorhandling'; 6 | 7 | // function that matches mutation type with query type 8 | export const matchMQ = async ( 9 | endpoint: string | URL, 10 | response?: any 11 | ): Promise => { 12 | let mutationArray: readonly mutationArray[] = []; 13 | let queryArray: readonly queryArray[] = []; 14 | 15 | const mutationIntrospect = await fetch(endpoint, { 16 | method: "POST", 17 | headers: { 18 | // need to change this later to account for variables 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ 22 | query: `{ 23 | __schema { 24 | mutationType{ 25 | name 26 | fields{ 27 | name 28 | type{ 29 | name 30 | kind 31 | ofType { 32 | name 33 | kind 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }`, 40 | }), 41 | }) 42 | .then((res) => res.json()) 43 | .then((data) => { 44 | mutationArray = data.data.__schema.mutationType.fields; 45 | }); 46 | 47 | 48 | const queryIntrospect = await fetch(endpoint, { 49 | method: "POST", 50 | headers: { 51 | // need to change this later to account for variables 52 | "Content-Type": "application/json", 53 | }, 54 | body: JSON.stringify({ 55 | query: `{ 56 | __schema { 57 | queryType{ 58 | name 59 | fields{ 60 | name 61 | type{ 62 | name 63 | kind 64 | ofType{ 65 | name 66 | } 67 | } 68 | } 69 | } 70 | } 71 | }`, 72 | }), 73 | }) 74 | .then((res) => res.json()) 75 | .then((data) => { 76 | queryArray = data.data.__schema.queryType.fields; 77 | }); 78 | console.log(queryArray, mutationArray); 79 | for (let i = 0; i < queryArray.length; i++) { 80 | for (let k = 0; k < mutationArray.length; k++) { 81 | if (queryArray[i].type.ofType.name === mutationArray[k].type.name) { 82 | console.log(queryArray[i], " matches with ", mutationArray[k]); 83 | console.log("Match found"); 84 | localStorage.removeItem(queryArray[i].name); 85 | console.log('Data is invalid, removed from cache'); 86 | } 87 | } 88 | } 89 | }; 90 | 91 | export const grabQueryName = (query: string): any => { 92 | // add checker to see if query type is a mutation 93 | try { 94 | // parses query using graphql-tag feature (makes an AST representation of the query) 95 | const parsedQuery: DocumentNode = gql` 96 | ${query} 97 | `; 98 | // check if mutation is present in parsed query 99 | // check if any of the objects in definitions array has a mutation, return true if so (some method returns a boolean) 100 | // parse query to extract name 101 | let NodeValue: string | null = null; 102 | // logic for checking what mutation is occurring and getting mutation type 103 | // traverse through AST using graphql's visit function 104 | visit(parsedQuery, { 105 | // this should be invoked whenever the visit function encounters an operation defintion node 106 | // here, we create operation definition key with associated method which is operationdefinition(node) 107 | OperationDefinition(node) { 108 | // if the node is a mutation and the value of the name property in node is defined 109 | // if there is a mutation, enter the selectionSet and access the selections arrays first element (which is an object) 110 | const firstSelection = node.selectionSet.selections[0]; 111 | // if firstSelection exists and the value of the kind property is "field" (which it has to be in order to have a name property) 112 | if (firstSelection && firstSelection.kind === "Field") { 113 | // set NodeValue to the name keys associated value 114 | NodeValue = firstSelection.name.value; 115 | } 116 | //} else { 117 | //console.error('Node operation is not a mutation!') 118 | //} 119 | }, 120 | }); 121 | return NodeValue; 122 | } catch (err) { 123 | if (err instanceof Error) { 124 | console.log(`${err}, Something went wrong when checking for mutations!`); 125 | return createClientError(err.message); 126 | } 127 | return null; 128 | } 129 | }; -------------------------------------------------------------------------------- /cacheiql-client/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ClientErrorType = { 2 | log: string; 3 | status: number; 4 | message: { err: string }; 5 | }; 6 | 7 | export type Query = string; 8 | 9 | export type Mutation = string; 10 | 11 | // define an interface for the various mutation types 12 | export interface MutationTypeSpecifier { 13 | delete: string[]; 14 | update: string[]; 15 | create: string[]; 16 | } 17 | 18 | export type mutationArray = { 19 | name: string; 20 | type: { name: string; kind: string }; 21 | }; 22 | 23 | export type queryArray = { 24 | name: string; 25 | type: { 26 | kind: string; 27 | name: null | string; 28 | ofType: { name: string }; 29 | }; 30 | }; 31 | 32 | // mutationTypes must match setup of MutationTypeSpecifier 33 | export const mutationTypes: MutationTypeSpecifier = { 34 | delete: ['delete', 'remove'], 35 | update: ['update', 'edit'], 36 | create: ['create', 'add', 'new', 'make'], 37 | }; 38 | 39 | // typing for main fetching/caching function, cacheiqIt (see export.ts) 40 | export type cacheiqItType = { 41 | endpoint: string | URL; 42 | query?: Query; 43 | mutation?: Mutation; 44 | time?: number; 45 | variables?: Object; 46 | }; 47 | -------------------------------------------------------------------------------- /cacheiql-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "compilerOptions": { 3 | // /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | // /* Projects */ 6 | // // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | // /* Language and Environment */ 14 | // "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | // /* Modules */ 28 | // "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./" /* Specify the root folder within your source files. */, 30 | // // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [ 34 | // "./", 35 | // "./dist" 36 | // ] /* Allow multiple folders to be treated as one when resolving modules. */, 37 | // // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 42 | // // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 43 | // // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 44 | // // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 45 | // // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 46 | // // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 47 | // // "resolveJsonModule": true, /* Enable importing .json files. */ 48 | // // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 49 | // // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 50 | 51 | // /* JavaScript Support */ 52 | // // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 53 | // // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 54 | // // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 55 | 56 | // /* Emit */ 57 | // // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 58 | // // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 59 | // // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 60 | // // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 61 | // // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 64 | // "outDir": "./dist" /* Specify an output folder for all emitted files. */, 65 | // // "removeComments": true, /* Disable emitting comments. */ 66 | // // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 67 | // // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 68 | // // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 69 | // // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 71 | // // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 72 | // // "newLine": "crlf", /* Set the newline character for emitting files. */ 73 | // // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 74 | // // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 75 | // // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 76 | // // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 77 | // // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 78 | 79 | // /* Interop Constraints */ 80 | // // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 81 | // // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 82 | // // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 83 | // // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 84 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 85 | // // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 86 | // "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 87 | 88 | // /* Type Checking */ 89 | // "strict": true /* Enable all strict type-checking options. */, 90 | // "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 91 | // // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 92 | // // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 93 | // // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 94 | // // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 95 | // // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 96 | // // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 97 | // // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 98 | // // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 99 | // // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 100 | // // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 101 | // // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 102 | // // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 103 | // // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 104 | // // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 105 | // // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 106 | // // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 107 | // // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 108 | // // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 109 | 110 | // /* Completeness */ 111 | // // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 112 | // "skipLibCheck": true /* Skip type checking all .d.ts files. */ 113 | // } 114 | // } 115 | 116 | { 117 | "compilerOptions": { 118 | "outDir": "./dist", // Where compiled files go 119 | "declaration": true, // Generate declaration files 120 | "declarationDir": "./dist", // Put declaration files in dist 121 | "emitDeclarationOnly": false, // Compile JS + declaration files 122 | "module": "CommonJS", // Module system for Node.js 123 | "moduleResolution": "Node", 124 | "target": "ESNext", // Output modern JS 125 | "rootDir": "./src", // Input folder for source files 126 | "strict": true, // Enable strict type-checking 127 | "esModuleInterop": true // Ensure compatibility with CommonJS 128 | }, 129 | "include": ["src/**/*"], // Include all files in src 130 | "exclude": ["node_modules", "dist"] // Exclude dependencies and output folder 131 | } 132 | -------------------------------------------------------------------------------- /cacheiql-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/cacheiql-server/.DS_Store -------------------------------------------------------------------------------- /cacheiql-server/README.md: -------------------------------------------------------------------------------- 1 | # cacheiql-server 2 | 3 | **cacheiql-server** is a middleware that is simple to implement, providing a server-side caching solution for GraphQL applications designed to enhance performance by reducing redundant query executions. It seamlessly integrates into your GraphQL server, caching queries and intelligently invalidating them after a mutation occurs, all while leveraging Redis to manage cache responses efficiently. 4 | 5 | 6 | --- 7 | 8 | ## Installation 9 | 10 | ### Installing and Connecting a Redis Server 11 | 12 | #### Redis Installation 13 | 14 | - **Mac (Homebrew):** 15 | 1. Open your terminal and run: 16 | ```bash 17 | brew install redis 18 | ``` 19 | 2. Start the Redis server with: 20 | ```bash 21 | redis-server 22 | ``` 23 | 3. Note the port number where the Redis server is listening. 24 | 25 | - **Linux or Non-Homebrew:** 26 | 1. Download the appropriate version of Redis from [redis.io/download](https://redis.io/download). 27 | 2. Follow the installation instructions provided for your system. 28 | 3. Start the Redis server and note the port number on which it is listening. 29 | 30 | 31 | 32 | ### Install CacheIQL-Server 33 | 34 | Install the package by running **`npm i cacheiql-server`** in your terminal. This will add **`cacheiql-server`** as a dependency in your package.json file. 35 | 36 | 37 | ## Implementation 38 | 39 | 1. **Import CacheIQL-Server** 40 | 41 | Add the following import to your Node.js/Express file: 42 | 43 | - **CommonJS:** 44 | ```js 45 | const { cacheMiddleware } = require('cacheiql-server'); 46 | ``` 47 | - **ES6+:** 48 | ```js 49 | import { cacheMiddleware } from 'cacheiql-server'; 50 | ``` 51 | 52 | 2. **Instantiate Cache Middleware** 53 | 54 | Create an instance of `cacheMiddleware` for your GraphQL endpoint by passing in the following parameters: 55 | 56 | - `rootValue`: Your GraphQL resolvers. 57 | - `TTL_IN_SECONDS`: Number of seconds data should persist in the Redis cache. 58 | - `graphqlSchema`: The GraphQL schema defined using the `graphql-JS` library. 59 | 60 | Example: 61 | ```js 62 | const TTL_IN_SECONDS = 1000; 63 | const cachedRootValue = cacheMiddleware(rootValue, TTL_IN_SECONDS, graphqlSchema); 64 | 3. **Add Cache Middleware to Express Route** 65 | 66 | Attach the `cachedRootValue` to the GraphQL route in your Express app like this: 67 | 68 | ```js 69 | app.use( 70 | "/graphql", 71 | graphqlHTTP({ 72 | schema: graphqlSchema, 73 | rootValue: cachedRootValue, 74 | graphiql: true, 75 | }) 76 | ); 77 | ``` 78 | This sets up the middleware to cache GraphQL query responses and invalidate them when a mutation occurs. 79 | 80 | 4. **Ensure Redis is Running** 81 | 82 | Make sure Redis is installed and running on your machine. 83 | Start the server using: 84 | ```bash 85 | redis-server 86 | ``` 87 | 5. **Start the Server** 88 | 89 | Run your Express server: 90 | ```bash 91 | node index.js 92 | ``` 93 | Visit [http://localhost:3000/graphql](http://localhost:3000/graphql) to access GraphQL and start testing the caching behavior. 94 | 95 | 6. **Example Implementation** 96 | 97 | Your Express server file should look something like this: 98 | 99 | ```js 100 | const express = require('express'); 101 | const { graphqlHTTP } = require('express-graphql'); 102 | const { cacheMiddleware } = require('cacheiql-server'); 103 | const graphqlSchema = require('./schema/schema'); 104 | const rootValue = require('./schema/resolvers'); 105 | 106 | const app = express(); 107 | 108 | const TTL_IN_SECONDS = 1000; 109 | const cachedRootValue = cacheMiddleware(rootValue, TTL_IN_SECONDS, graphqlSchema); 110 | 111 | app.use( 112 | "/graphql", 113 | graphqlHTTP({ 114 | schema: graphqlSchema, 115 | rootValue: cachedRootValue, 116 | graphiql: true, 117 | }) 118 | ); 119 | 120 | app.listen(3000, () => console.log('listening on 3000')); 121 | ``` 122 | This sets up cacheMiddleware to cache query responses and intelligently invalidate them after mutations, enhancing the performance of your GraphQL application. 123 | -------------------------------------------------------------------------------- /cacheiql-server/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | transform: { 5 | "^.+\\.tsx?$": ["ts-jest", { isolatedModules: true }], // Move ts-jest config here 6 | }, 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 8 | testMatch: ["**/tests/**/*.test.ts"], // Match test files 9 | }; 10 | -------------------------------------------------------------------------------- /cacheiql-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cacheiql-server", 3 | "version": "1.0.0", 4 | "description": "Server side cacheiql", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/CacheIQL.git#readme" 12 | }, 13 | "keywords": [ 14 | "graphQL", 15 | "server", 16 | "redis", 17 | "OSP" 18 | ], 19 | "author": "Vasean Annin, George German, Gabriella Davoudpour, Pedram Kashani, Chris Matzen", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/oslabs-beta/CacheIQL/issues" 23 | }, 24 | "homepage": "https://github.com/oslabs-beta/CacheIQL/tree/readme#readme", 25 | "dependencies": { 26 | "redis": "^4.7.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^22.10.1", 30 | "jest": "^29.7.0", 31 | "ts-jest": "^29.2.5", 32 | "typescript": "^5.7.2" 33 | }, 34 | "peerDependencies": { 35 | "graphql": "^15.10.1" 36 | }, 37 | "resolutions": { 38 | "graphql": "^15.10.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cacheiql-server/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/cacheiql-server/src/.DS_Store -------------------------------------------------------------------------------- /cacheiql-server/src/cache/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/cacheiql-server/src/cache/.DS_Store -------------------------------------------------------------------------------- /cacheiql-server/src/cache/cacheManager.ts: -------------------------------------------------------------------------------- 1 | import { getRedisClient, connectRedis } from './redisClient'; 2 | import { entityRelationships } from "../schema/introspection"; 3 | 4 | 5 | (async () => { 6 | await connectRedis(); 7 | })(); 8 | 9 | 10 | interface CacheOptions { 11 | ttl?: number; // Time to live in seconds 12 | } 13 | export let cacheHits = 0; 14 | export let cacheMisses = 0; 15 | 16 | /** 17 | * Caches a query result in Redis. 18 | * @param key - The key under which the data will be stored. 19 | * @param data - The data to be cached. 20 | * @param entity - The GraphQL entity for cache tracking. 21 | * @param options - Cache options (e.g., TTL). 22 | */ 23 | 24 | export const setCacheQuery = async ( 25 | key: string, 26 | data: any, 27 | entity: string, 28 | options: CacheOptions = { ttl: 60} 29 | ) => { 30 | try { 31 | const client = await getRedisClient(); 32 | const namespacedKey = `myApp:${key}`; 33 | if (data !== undefined) { 34 | await client.set(namespacedKey, JSON.stringify(data)); 35 | await client.expire(namespacedKey, options.ttl ?? 60); 36 | await trackCacheDependency(namespacedKey, entity); 37 | } else { 38 | console.warn(`Skipping cache set for ${key} due to undefined data`); 39 | } 40 | } catch (error) { 41 | console.error(`Error caching query for key "${key}":`, error); 42 | throw new Error(`Cache operation failed for key "${key}"`); 43 | } 44 | }; 45 | 46 | /** 47 | * Retrieves a cached query result from Redis. 48 | * @param key - The key to retrieve. 49 | * @returns The cached data or null if not found. 50 | */ 51 | export const getCachedQuery = async ( 52 | key: string 53 | ): Promise => { 54 | try { 55 | const client = await getRedisClient(); 56 | const cachedData = await client.get(`myApp:${key}`); 57 | if (cachedData) { 58 | cacheHits++; 59 | try { 60 | return JSON.parse(cachedData); 61 | } catch (error) { 62 | console.error(`Error parsing cached data for key "${key}":`, error); 63 | return null; 64 | } 65 | } 66 | cacheMisses++; 67 | return null; 68 | } catch (error) { 69 | console.error(`Error retrieving cache for key "${key}":`, error); 70 | return null; 71 | } 72 | }; 73 | 74 | /** 75 | * Invalidates a specific cache key. 76 | * @param key - The key to remove from cache. 77 | */ 78 | export const invalidateCache = async (key: string) => { 79 | try { 80 | const client = await getRedisClient(); 81 | await client.del(`myApp:${key}`); 82 | } catch (error) { 83 | console.error(`Error invalidating cache for key "${key}":`, error); 84 | throw error; 85 | } 86 | }; 87 | 88 | 89 | /** 90 | * Retrieves data from cache or fetches from the database. 91 | * @param key - The cache key. 92 | * @param entity - The entity name for cache tracking. 93 | * @param fetchFromDb - Function to fetch data from the database if not in cache. 94 | * @returns The data from cache or database. 95 | */ 96 | export const getData = async ( 97 | key: string, 98 | entity: string, 99 | fetchFromDb: () => Promise 100 | ) => { 101 | try { 102 | const cacheData = await getCachedQuery(key); 103 | if (cacheData) { 104 | console.log("returning data from cache"); 105 | return cacheData; 106 | } 107 | const dbData = await fetchFromDb(); 108 | await setCacheQuery(key, dbData, entity, {ttl:60}); 109 | return dbData; 110 | } catch (error) { 111 | console.error("Error fetching data", error); 112 | throw error; 113 | } 114 | }; 115 | 116 | 117 | /** 118 | * Tracks cache dependencies per entity. 119 | * This ensures that when an entity changes, only affected fields are invalidated. 120 | * Now also tracks relationships based on introspection. 121 | * @param cacheKey - The cache key to track. 122 | * @param entity - The entity name. 123 | */ 124 | 125 | export const trackCacheDependency = async ( 126 | cacheKey: string, 127 | entity: string 128 | ) => { 129 | try { 130 | const client = await getRedisClient(); 131 | const trackingKey = `dependencyKeys:${entity}`; 132 | await client.sAdd(trackingKey, cacheKey); 133 | const relatedEntities = entityRelationships[entity] || []; 134 | for (const relatedEntity of relatedEntities) { 135 | const relatedTrackingKey = `dependencyKeys:${relatedEntity}`; 136 | await client.sAdd(relatedTrackingKey, cacheKey); 137 | await client.sAdd(trackingKey, `dependencyKeys:${relatedEntity}`); 138 | } 139 | } catch (error) { 140 | console.error( 141 | `Error tracking cache dependency for entity "${entity}":`, 142 | error 143 | ); 144 | } 145 | }; 146 | 147 | 148 | /** 149 | * Invalidates cache entries for a specific entity after a mutation. 150 | * @param entity - The entity whose cache entries should be invalidated. 151 | */ 152 | 153 | export const invalidateCacheForMutation = async (entity: string) => { 154 | try { 155 | const client = await getRedisClient(); 156 | const trackingKey = `dependencyKeys:${entity}`; 157 | let cacheKeys: string[] = await client.sMembers(trackingKey); 158 | const relatedEntities = entityRelationships[entity] || []; 159 | for (const relatedEntity of relatedEntities) { 160 | const relatedTrackingKey = `dependencyKeys:${relatedEntity}`; 161 | const relatedKeys: string[] = await client.sMembers(relatedTrackingKey); 162 | cacheKeys.push(...relatedKeys); 163 | } 164 | if (cacheKeys.length > 0) { 165 | await Promise.all(cacheKeys.map((key) => client.del(key))); 166 | console.log( 167 | `Invalidated ${cacheKeys.length} cache keys for ${entity} and related entities.` 168 | ); 169 | } else { 170 | console.log(`No cache keys found for entity: ${entity}`); 171 | } 172 | await client.del(trackingKey); 173 | for (const relatedEntity of relatedEntities) { 174 | const relatedTrackingKey = `dependencyKeys:${relatedEntity}`; 175 | await client.del(relatedTrackingKey); 176 | } 177 | console.log( 178 | `Dependency tracking removed for ${entity} and related entities.` 179 | ); 180 | } catch (error) { 181 | console.error(`Error invalidating cache for entity "${entity}":`, error); 182 | } 183 | }; 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /cacheiql-server/src/cache/cacheUtils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | export const hashKey = (string: string): string => { 4 | return crypto.createHash("sha256").update(string).digest("hex").slice(0, 64); 5 | }; 6 | -------------------------------------------------------------------------------- /cacheiql-server/src/cache/redisClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient, RedisClientType } from 'redis'; 2 | let client: RedisClientType | undefined 3 | 4 | /** 5 | * Connects to the Redis server 6 | * @param url - The Redis server URL (default: redis://localhost:6379) 7 | */ 8 | export const connectRedis = async ( 9 | url: string = process.env.REDIS_URL || 'redis://localhost:6379' 10 | ): Promise => { 11 | if (client && client.isOpen) return; 12 | try { 13 | client = createClient({ url }); 14 | 15 | client.on("error", (err) => { 16 | console.error("Redis Client Error:", err); 17 | client = undefined; 18 | setTimeout(() => connectRedis(url), 5000); 19 | }); 20 | client.on("ready", () => { 21 | console.log("Redis is ready and connected."); 22 | }); 23 | await client.connect(); 24 | } catch (error) { 25 | console.error("Failed to connect to Redis:", error); 26 | client = undefined 27 | } 28 | }; 29 | 30 | /** 31 | * Returns the Redis client instance 32 | * @throws Error if the client is not connected 33 | */ 34 | export const getRedisClient = async (): Promise => { 35 | if (!client || !client.isOpen) { 36 | console.warn("Redis client not connected. Attempting to reconnect..."); 37 | await connectRedis(); // Ensure connection is complete before returning 38 | } 39 | if (!client) throw new Error("Redis client is not available."); 40 | return client; 41 | }; 42 | 43 | 44 | 45 | /** 46 | * Disconnects the Redis client 47 | */ 48 | export const closeRedisConnection = async (): Promise => { 49 | if (client && client.isOpen) { 50 | await client.disconnect(); 51 | console.log('Redis connection closed'); 52 | } 53 | }; 54 | 55 | -------------------------------------------------------------------------------- /cacheiql-server/src/cli/generateSchema.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/cacheiql-server/src/cli/generateSchema.ts -------------------------------------------------------------------------------- /cacheiql-server/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | //CLI entry point -------------------------------------------------------------------------------- /cacheiql-server/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/cacheiql-server/src/index.ts -------------------------------------------------------------------------------- /cacheiql-server/src/middleware/cacheMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setCacheQuery, 3 | getCachedQuery, 4 | trackCacheDependency, 5 | invalidateCacheForMutation, 6 | } from "../cache/cacheManager"; 7 | import { 8 | extractEntityRelationships, 9 | entityRelationships, 10 | } from "../schema/introspection"; 11 | import { GraphQLResolveInfo, GraphQLSchema } from "graphql"; 12 | import { hashKey } from "../cache/cacheUtils"; 13 | import { parseQueryFields } from "../query/queryParser"; 14 | 15 | 16 | let introspectionInitialized = false; 17 | 18 | export const cacheMiddleware = ( 19 | rootValue: { [key: string]: Function }, 20 | ttl: number = 60, 21 | schema?: GraphQLSchema 22 | ) => { 23 | if (!introspectionInitialized && schema) { 24 | console.log( 25 | "Running GraphQL Introspection to extract entity relationships..." 26 | ); 27 | extractEntityRelationships(schema); 28 | introspectionInitialized = true; 29 | } 30 | const wrappedResolvers: { [key: string]: Function } = {}; 31 | Object.keys(rootValue).forEach((key) => { 32 | const resolve = rootValue[key]; 33 | wrappedResolvers[key] = async ( 34 | parent: any, 35 | args: any, 36 | info?: GraphQLResolveInfo, 37 | context?: any 38 | ): Promise => { 39 | if (!info) { 40 | console.error( 41 | "Missing GraphQLResolveInfo in cacheMiddleware. Skipping caching." 42 | ); 43 | return await resolve(parent, args, info, context); 44 | } 45 | const parsedFields = parseQueryFields(info); 46 | const entityType = info.returnType.toString().replace(/[[\]!]/g, ""); // Extract entity name 47 | const entity = entityType.charAt(0).toUpperCase() + entityType.slice(1); // Capitalize first letter 48 | const sortedArgs = JSON.stringify(args, Object.keys(args).sort()); // Ensure consistent key order 49 | const rawKey = `${entity}:${ 50 | info.fieldName 51 | }:${sortedArgs}:${JSON.stringify(parsedFields)}`; 52 | const cacheKey = hashKey(rawKey); 53 | 54 | // Handle Queries (Caching) 55 | if (info.operation?.operation === "query") { 56 | try { 57 | // Try to get cached data 58 | const cachedData = await getCachedQuery(cacheKey); 59 | if (cachedData) { 60 | return cachedData; 61 | } 62 | // Execute resolver and cache the result 63 | const result = await resolve(parent, args, context, info); 64 | await setCacheQuery(cacheKey, result, entity, { ttl }); 65 | // Track dependencies for cache invalidation 66 | await trackCacheDependency(cacheKey, entity); 67 | return result; 68 | } catch (error) { 69 | console.error(`Error processing query ${info.fieldName}:`, error); 70 | throw new Error( 71 | `Failed to resolve ${info.fieldName}. See logs for details.` 72 | ); 73 | } 74 | } 75 | // Handle Mutations (Cache Invalidation) 76 | if (info.operation?.operation === "mutation") { 77 | try { 78 | // Execute mutation first 79 | const result = await resolve(parent, args, context, info); 80 | // Invalidate cache for the main entity and related entities 81 | await invalidateCacheForMutation(entity); 82 | return result; 83 | } catch (error) { 84 | console.error( 85 | `Error processing mutation "${info.fieldName}" for entity "${entity}":`, 86 | error 87 | ); 88 | throw new Error(`Mutation failed for ${info.fieldName}. Check logs.`); 89 | } 90 | } 91 | // Default case: If the operation is not a query or mutation 92 | return await resolve(parent, args, context, info); 93 | }; 94 | }); 95 | return wrappedResolvers; 96 | }; 97 | 98 | 99 | -------------------------------------------------------------------------------- /cacheiql-server/src/query/mergeUtils.ts: -------------------------------------------------------------------------------- 1 | // Merges cached and newly fetched data 2 | /** 3 | * Merges cached data with newly fetched data. 4 | * Ensures that cached fields are preserved and only missing fields are added. 5 | * 6 | * @param cachedData - The cached GraphQL response. 7 | * @param newData - The newly fetched partial response. 8 | * @returns The final merged response. 9 | */ 10 | export function mergeCachedAndNewData( 11 | cachedData: Record, 12 | newData: Record 13 | ): Record { 14 | if (!newData) return cachedData; // No new data, return cache 15 | 16 | const mergedData: Record = { ...cachedData }; 17 | 18 | Object.keys(newData).forEach((key) => { 19 | if (Array.isArray(newData[key]) && Array.isArray(cachedData[key])) { 20 | // If both are arrays, merge unique items 21 | mergedData[key] = [...new Set([...cachedData[key], ...newData[key]])]; 22 | } else if (typeof newData[key] === "object" && typeof cachedData[key] === "object") { 23 | // If both are objects, merge deeply 24 | mergedData[key] = mergeCachedAndNewData(cachedData[key], newData[key]); 25 | } else { 26 | // Otherwise, prefer new data 27 | mergedData[key] = newData[key] ?? cachedData[key]; 28 | } 29 | }); 30 | 31 | return mergedData; 32 | } 33 | -------------------------------------------------------------------------------- /cacheiql-server/src/query/partialResolver.ts: -------------------------------------------------------------------------------- 1 | // //Resolves partial queries for uncached data 2 | // import { GraphQLResolveInfo, graphql, GraphQLSchema } from "graphql"; 3 | // import { extractEntities } from "../query/queryParser"; 4 | // import { getNamedType, isObjectType } from "graphql"; 5 | // import { 6 | // setCacheQuery, 7 | // getCachedQuery, 8 | // invalidateCacheForMutation, 9 | // } from "../cache/cacheManager"; 10 | 11 | 12 | // import { mergeCachedAndNewData } from "../query/mergeUtils"; 13 | 14 | // /** 15 | // * Resolves uncached fields by making a partial query. 16 | // * @param schema - The GraphQL schema object. 17 | // * @param info - GraphQLResolveInfo from the resolver. 18 | // * @param cachedData - The cached result (if available). 19 | // * @returns The merged response (cached + newly fetched). 20 | // */ 21 | // export async function resolvePartialQuery( 22 | // schema: GraphQLSchema, 23 | // info: GraphQLResolveInfo, 24 | // cachedData: Record 25 | // ): Promise> { 26 | // const missingFields: Set = new Set(); 27 | // const rootType = info.parentType; 28 | 29 | // // Step 1: Identify missing fields 30 | // info.fieldNodes.forEach((field) => { 31 | // const fieldDef = rootType.getFields()[field.name.value]; 32 | 33 | // if (fieldDef) { 34 | // const entityType = getNamedType(fieldDef.type); 35 | // if (isObjectType(entityType)) { 36 | // const fieldName = field.name.value; 37 | // if (!cachedData[fieldName]) { 38 | // missingFields.add(fieldName); 39 | // } 40 | // } 41 | // } 42 | // }); 43 | 44 | // // Step 2: If all fields are cached, return cached data immediately 45 | // if (missingFields.size === 0) { 46 | // return cachedData; 47 | // } 48 | 49 | // // Step 3: Construct a new partial query for only missing fields 50 | // const partialQuery = ` 51 | // { 52 | // ${Array.from(missingFields).join("\n")} 53 | // } 54 | // `; 55 | 56 | // // Step 4: Execute partial query 57 | // const newData = await graphql({ 58 | // schema, 59 | // source: partialQuery, 60 | // }); 61 | 62 | // // Step 5: Merge cached + new data 63 | // return mergeCachedAndNewData(cachedData, newData.data); 64 | // } 65 | -------------------------------------------------------------------------------- /cacheiql-server/src/query/queryParser.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo, SelectionNode, FieldNode, FragmentSpreadNode, InlineFragmentNode } from "graphql"; 2 | 3 | /** 4 | * Parses a GraphQL query and extracts fields and subfields. 5 | * @param info GraphQLResolveInfo from the resolver. 6 | * @returns A structured representation of the query fields. 7 | */ 8 | export const parseQueryFields = (info: GraphQLResolveInfo): Record => { 9 | return collectFields(info, info.fieldNodes); 10 | }; 11 | 12 | /** 13 | * Recursively collects fields from the GraphQLResolveInfo object. 14 | * @param info GraphQLResolveInfo 15 | * @param nodes Array of selection nodes 16 | * @param path Path of the current field (for nested structures) 17 | * @returns A structured object representing the fields and subfields. 18 | */ 19 | const collectFields = (info: GraphQLResolveInfo, nodes: readonly SelectionNode[], path: string = ""): Record => { 20 | const fields: Record = {}; 21 | 22 | for (const node of nodes) { 23 | if (node.kind === "Field") { 24 | const fieldNode = node as FieldNode; 25 | const fieldName = fieldNode.name.value; 26 | 27 | if (fieldNode.selectionSet) { 28 | fields[fieldName] = collectFields(info, fieldNode.selectionSet.selections, path + "." + fieldName); 29 | } else { 30 | fields[fieldName] = true; 31 | } 32 | } else if (node.kind === "FragmentSpread") { 33 | const fragmentNode = info.fragments[(node as FragmentSpreadNode).name.value]; 34 | if (fragmentNode) { 35 | Object.assign(fields, collectFields(info, fragmentNode.selectionSet.selections, path)); 36 | } 37 | } else if (node.kind === "InlineFragment") { 38 | Object.assign(fields, collectFields(info, (node as InlineFragmentNode).selectionSet.selections, path)); 39 | } 40 | } 41 | 42 | return fields; 43 | }; 44 | -------------------------------------------------------------------------------- /cacheiql-server/src/schema/introspection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getIntrospectionQuery, 3 | graphql, 4 | GraphQLSchema, 5 | getNamedType, 6 | isObjectType, 7 | } from "graphql"; 8 | 9 | /** 10 | * Stores entity relationships globally to be used by cache tracking 11 | */ 12 | export let entityRelationships: Record = {}; 13 | 14 | /** 15 | * Fetches and returns the GraphQL schema introspection result. 16 | * @param schema - The GraphQLSchema object. 17 | * @returns The introspection JSON result. 18 | */ 19 | export async function getSchemaIntrospection(schema: GraphQLSchema) { 20 | try { 21 | const result = await graphql({ 22 | schema, 23 | source: getIntrospectionQuery(), 24 | }); 25 | 26 | if (result.errors) { 27 | console.error("GraphQL Introspection Error:", result.errors); 28 | } 29 | 30 | return result; 31 | } catch (error) { 32 | console.error("Failed to fetch introspection data:", error); 33 | throw error; 34 | } 35 | } 36 | 37 | /** 38 | * Extracts entity relationships from a GraphQL schema and stores them. 39 | * @param schema - The GraphQLSchema object. 40 | */ 41 | export function extractEntityRelationships(schema: GraphQLSchema) { 42 | 43 | const typeMap = schema.getTypeMap(); 44 | const relationships: Record = {}; 45 | 46 | for (const typeName in typeMap) { 47 | const type = typeMap[typeName]; 48 | 49 | if (isObjectType(type)) { 50 | const fields = type.getFields(); 51 | relationships[typeName] = Object.values(fields) 52 | .map((field) => 53 | getNamedType(field.type) 54 | .toString() 55 | .replace(/[[\]!]/g, "") 56 | ) 57 | .filter((relatedType) => relatedType !== typeName); 58 | } 59 | } 60 | 61 | entityRelationships = relationships; 62 | console.log("Extracted Entity Relationships:", entityRelationships); 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /cacheiql-server/src/schema/schemaLoader.ts: -------------------------------------------------------------------------------- 1 | // Loads schema JSON files for use -------------------------------------------------------------------------------- /cacheiql-server/src/schema/validation.ts: -------------------------------------------------------------------------------- 1 | // Validates provided schema JSON -------------------------------------------------------------------------------- /cacheiql-server/tests/cacheManager.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const cacheManager_1 = require("../src/cacheManager"); 13 | const redisClient_1 = require("../src/redisClient"); 14 | // ----------------- Integration Tests ----------------- 15 | describe('Cache Manager Integration Tests', () => { 16 | const key = 'test:key'; 17 | const value = { message: 'Hello, CacheIQL!' }; 18 | beforeAll(() => __awaiter(void 0, void 0, void 0, function* () { 19 | yield (0, redisClient_1.connectRedis)(); 20 | })); 21 | afterAll(() => __awaiter(void 0, void 0, void 0, function* () { 22 | yield (0, redisClient_1.closeRedisConnection)(); 23 | })); 24 | it('should cache a query result', () => __awaiter(void 0, void 0, void 0, function* () { 25 | yield (0, cacheManager_1.setCacheQuery)(key, value); 26 | const cachedData = yield (0, cacheManager_1.getCachedQuery)(key); 27 | expect(cachedData).toEqual(value); 28 | })); 29 | it('should return null for a non-existing cache key', () => __awaiter(void 0, void 0, void 0, function* () { 30 | const cachedData = yield (0, cacheManager_1.getCachedQuery)('non-existing:key'); 31 | expect(cachedData).toBeNull(); 32 | })); 33 | it('should invalidate a cached query', () => __awaiter(void 0, void 0, void 0, function* () { 34 | yield (0, cacheManager_1.invalidateCache)(key); 35 | const cachedData = yield (0, cacheManager_1.getCachedQuery)(key); 36 | expect(cachedData).toBeNull(); 37 | })); 38 | }); 39 | // ----------------- Unit Tests with Mocking ----------------- 40 | jest.mock('../src/redisClient', () => { 41 | const originalModule = jest.requireActual('../src/redisClient'); 42 | return Object.assign(Object.assign({}, originalModule), { getRedisClient: jest.fn() }); 43 | }); 44 | describe('Cache Manager Unit Tests with Mocking', () => { 45 | it('cacheQuery should cache data with the specified key', () => __awaiter(void 0, void 0, void 0, function* () { 46 | //ensures that when mockClient.set is called, it behaves like an async function (Promise) that resolves with 'OK'. 47 | const mockClient = { 48 | set: jest.fn().mockResolvedValue('OK'), 49 | }; 50 | //Ensures cacheQuery interacts with the mocked Redis client instead of a real one. 51 | redisClient_1.getRedisClient.mockReturnValue(mockClient); 52 | yield (0, cacheManager_1.setCacheQuery)('testKey', { value: 42 }); 53 | expect(mockClient.set).toHaveBeenCalledWith('myApp:testKey', JSON.stringify({ value: 42 }), { EX: 3600 }); 54 | })); 55 | it('getCachedQuery should return cached data if it exists', () => __awaiter(void 0, void 0, void 0, function* () { 56 | const mockClient = { 57 | get: jest.fn().mockResolvedValue(JSON.stringify({ value: 42 })), 58 | }; 59 | redisClient_1.getRedisClient.mockReturnValue(mockClient); 60 | const result = yield (0, cacheManager_1.getCachedQuery)('testKey'); 61 | expect(mockClient.get).toHaveBeenCalledWith('testKey'); 62 | expect(result).toEqual({ value: 42 }); 63 | })); 64 | it('getCachedQuery should return null if data is not cached', () => __awaiter(void 0, void 0, void 0, function* () { 65 | const mockClient = { 66 | get: jest.fn().mockResolvedValue(null), 67 | }; 68 | redisClient_1.getRedisClient.mockReturnValue(mockClient); 69 | const result = yield (0, cacheManager_1.getCachedQuery)('nonExistingKey'); 70 | expect(mockClient.get).toHaveBeenCalledWith('nonExistingKey'); 71 | expect(result).toBeNull(); 72 | })); 73 | it('invalidateCache should delete cached data for the specified key', () => __awaiter(void 0, void 0, void 0, function* () { 74 | const mockClient = { 75 | del: jest.fn().mockResolvedValue(1), 76 | }; 77 | redisClient_1.getRedisClient.mockReturnValue(mockClient); 78 | yield (0, cacheManager_1.invalidateCache)('testKey'); 79 | expect(mockClient.del).toHaveBeenCalledWith('testKey'); 80 | })); 81 | }); 82 | -------------------------------------------------------------------------------- /cacheiql-server/tests/cacheManager.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setCacheQuery, 3 | getCachedQuery, 4 | invalidateCache, 5 | } from '../src/cacheManager'; 6 | import { 7 | connectRedis, 8 | closeRedisConnection, 9 | getRedisClient, 10 | } from '../src/redisClient'; 11 | 12 | // ----------------- Integration Tests ----------------- 13 | describe('Cache Manager Integration Tests', () => { 14 | const key = 'test:key'; 15 | const value = { message: 'Hello, CacheIQL!' }; 16 | 17 | beforeAll(async () => { 18 | await connectRedis(); 19 | }); 20 | 21 | afterAll(async () => { 22 | await closeRedisConnection(); 23 | }); 24 | 25 | it('should cache a query result', async () => { 26 | await setCacheQuery(key, value); 27 | const cachedData = await getCachedQuery(key); 28 | expect(cachedData).toEqual(value); 29 | }); 30 | 31 | it('should return null for a non-existing cache key', async () => { 32 | const cachedData = await getCachedQuery('non-existing:key'); 33 | expect(cachedData).toBeNull(); 34 | }); 35 | 36 | it('should invalidate a cached query', async () => { 37 | await invalidateCache(key); 38 | const cachedData = await getCachedQuery(key); 39 | expect(cachedData).toBeNull(); 40 | }); 41 | }); 42 | 43 | // ----------------- Unit Tests with Mocking ----------------- 44 | jest.mock('../src/redisClient', () => { 45 | const originalModule = jest.requireActual('../src/redisClient'); 46 | return { 47 | ...originalModule, // Keeps `connectRedis` and `closeRedisConnection` unchanged 48 | getRedisClient: jest.fn(), // Mock specific methods 49 | }; 50 | }); 51 | 52 | describe('Cache Manager Unit Tests with Mocking', () => { 53 | it('cacheQuery should cache data with the specified key', async () => { 54 | //ensures that when mockClient.set is called, it behaves like an async function (Promise) that resolves with 'OK'. 55 | const mockClient = { 56 | set: jest.fn().mockResolvedValue('OK'), 57 | }; 58 | //Ensures cacheQuery interacts with the mocked Redis client instead of a real one. 59 | (getRedisClient as jest.Mock).mockReturnValue(mockClient); 60 | 61 | await setCacheQuery('testKey', { value: 42 }); 62 | expect(mockClient.set).toHaveBeenCalledWith( 63 | 'myApp:testKey', 64 | JSON.stringify({ value: 42 }), 65 | { EX: 3600 } 66 | ); 67 | }); 68 | 69 | it('getCachedQuery should return cached data if it exists', async () => { 70 | const mockClient = { 71 | get: jest.fn().mockResolvedValue(JSON.stringify({ value: 42 })), 72 | }; 73 | (getRedisClient as jest.Mock).mockReturnValue(mockClient); 74 | 75 | const result = await getCachedQuery('testKey'); 76 | expect(mockClient.get).toHaveBeenCalledWith('testKey'); 77 | expect(result).toEqual({ value: 42 }); 78 | }); 79 | 80 | it('getCachedQuery should return null if data is not cached', async () => { 81 | const mockClient = { 82 | get: jest.fn().mockResolvedValue(null), 83 | }; 84 | (getRedisClient as jest.Mock).mockReturnValue(mockClient); 85 | 86 | const result = await getCachedQuery('nonExistingKey'); 87 | expect(mockClient.get).toHaveBeenCalledWith('nonExistingKey'); 88 | expect(result).toBeNull(); 89 | }); 90 | 91 | it('invalidateCache should delete cached data for the specified key', async () => { 92 | const mockClient = { 93 | del: jest.fn().mockResolvedValue(1), 94 | }; 95 | (getRedisClient as jest.Mock).mockReturnValue(mockClient); 96 | 97 | await invalidateCache('testKey'); 98 | expect(mockClient.del).toHaveBeenCalledWith('testKey'); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /cacheiql-server/tests/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | describe("Index Tests", () => { 3 | it("should run a basic test", () => { 4 | expect(true).toBe(true); // Placeholder test 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /cacheiql-server/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | describe("Index Tests", () => { 2 | it("should run a basic test", () => { 3 | expect(true).toBe(true); // Placeholder test 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /cacheiql-server/tests/redisClient.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const redisClient_1 = require("../src/redisClient"); 13 | describe("Redis Client", () => { 14 | beforeAll(() => __awaiter(void 0, void 0, void 0, function* () { 15 | yield (0, redisClient_1.connectRedis)(); 16 | })); 17 | afterAll(() => __awaiter(void 0, void 0, void 0, function* () { 18 | yield (0, redisClient_1.closeRedisConnection)(); 19 | })); 20 | it("should connect to Redis and return a client instance", () => { 21 | const client = (0, redisClient_1.getRedisClient)(); 22 | expect(client).toBeDefined(); 23 | }); 24 | it("should throw an error if Redis client is not connected", () => __awaiter(void 0, void 0, void 0, function* () { 25 | yield (0, redisClient_1.closeRedisConnection)(); // Close the connection 26 | expect(() => (0, redisClient_1.getRedisClient)()).toThrowError("Redis client is not connected"); 27 | // Reconnect for subsequent tests 28 | yield (0, redisClient_1.connectRedis)(); 29 | })); 30 | }); 31 | -------------------------------------------------------------------------------- /cacheiql-server/tests/redisClient.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | connectRedis, 4 | getRedisClient, 5 | closeRedisConnection, 6 | } from "../src/redisClient"; 7 | 8 | describe("Redis Client", () => { 9 | beforeAll(async () => { 10 | await connectRedis(); 11 | }); 12 | 13 | afterAll(async () => { 14 | await closeRedisConnection(); 15 | }); 16 | 17 | it("should connect to Redis and return a client instance", () => { 18 | const client = getRedisClient(); 19 | expect(client).toBeDefined(); 20 | }); 21 | 22 | it("should throw an error if Redis client is not connected", async () => { 23 | await closeRedisConnection(); // Close the connection 24 | expect(() => getRedisClient()).toThrowError( 25 | "Redis client is not connected" 26 | ); 27 | 28 | // Reconnect for subsequent tests 29 | await connectRedis(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /cacheiql-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "compilerOptions": { 3 | 4 | // /* Visit https://aka.ms/tsconfig to read more about this file */ 5 | 6 | // /* Projects */ 7 | // // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 8 | // // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 10 | // // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 11 | // // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | // /* Language and Environment */ 15 | // "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 16 | // // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | // /* Modules */ 29 | // "module": "commonjs" /* Specify what module code is generated. */, 30 | // "rootDir": "./src" /* Specify the root folder within your source files. */, 31 | // // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 32 | // // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 40 | // // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 41 | // // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 45 | // // "resolveJsonModule": true, /* Enable importing .json files. */ 46 | // // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 47 | // // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 48 | 49 | // /* JavaScript Support */ 50 | // // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 51 | // // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 52 | // // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 53 | 54 | // /* Emit */ 55 | // // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 56 | // // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 57 | // // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 58 | // // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 59 | // // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 62 | // "outDir": "./dist" /* Specify an output folder for all emitted files. */, 63 | // // "removeComments": true, /* Disable emitting comments. */ 64 | // // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 65 | // // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | 77 | // /* Interop Constraints */ 78 | // // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 79 | // // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 80 | // // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 81 | // // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 82 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 83 | // // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 84 | // "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 85 | 86 | // /* Type Checking */ 87 | // "strict": true /* Enable all strict type-checking options. */, 88 | // "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 89 | // // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 90 | // // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 91 | // // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 92 | // // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 93 | // // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 94 | // // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 95 | // // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 96 | // // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 97 | // // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 98 | // // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 99 | // // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 100 | // // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 101 | // // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 102 | // // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 103 | // // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 104 | // // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 105 | // // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 106 | // // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 107 | 108 | // /* Completeness */ 109 | // // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 110 | // "skipLibCheck": true /* Skip type checking all .d.ts files. */ 111 | // } 112 | // } 113 | { 114 | "compilerOptions": { 115 | "outDir": "./dist", // Where compiled files go 116 | "declaration": true, // Generate declaration files 117 | "declarationDir": "./dist", // Put declaration files in dist 118 | "emitDeclarationOnly": false, // Compile JS + declaration files 119 | "module": "CommonJS", // Module system for Node.js 120 | "moduleResolution": "Node", 121 | "target": "ESNext", // Output modern JS 122 | "rootDir": "./src", // Input folder for source files 123 | "strict": true, // Enable strict type-checking 124 | "esModuleInterop": true // Ensure compatibility with CommonJS 125 | }, 126 | "include": ["src/**/*"], // Include all files in src 127 | "exclude": ["node_modules", "dist"] // Exclude dependencies and output folder 128 | } 129 | -------------------------------------------------------------------------------- /test-client/distTSC/client/components/CharacterCard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const jsx_runtime_1 = require("react/jsx-runtime"); 4 | const CharacterCard = ({ character }) => { 5 | return ((0, jsx_runtime_1.jsxs)("div", { className: 'characterCard', children: [(0, jsx_runtime_1.jsx)("h1", { children: character.name }), (0, jsx_runtime_1.jsxs)("p", { children: ["Gender: ", character.gender] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Hair Color: ", character.hair_color] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Birth Year: ", character.birth_year] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Homeplanet: ", character.homeworld_id] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Species: ", character.species_id] })] })); 6 | }; 7 | exports.default = CharacterCard; 8 | -------------------------------------------------------------------------------- /test-client/distTSC/client/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const jsx_runtime_1 = require("react/jsx-runtime"); 16 | const CharacterCard_1 = __importDefault(require("./CharacterCard")); 17 | const ReviewCard_1 = __importDefault(require("./ReviewCard")); 18 | const react_1 = require("react"); 19 | const HitMiss_1 = __importDefault(require("./HitMiss")); 20 | const { cacheIt } = require('../../../../cacheiql-client/dist/index'); 21 | const Dashboard = () => { 22 | const peopleArray = []; 23 | const reviewArray = []; 24 | const [characterinfo, setCharacterinfo] = (0, react_1.useState)(peopleArray); 25 | const [reviewInfo, setReviewInfo] = (0, react_1.useState)(reviewArray); 26 | const [reviewText, setReviewText] = (0, react_1.useState)(''); 27 | const [time, setTime] = (0, react_1.useState)(0); 28 | const getPeopleB = () => __awaiter(void 0, void 0, void 0, function* () { 29 | const startTime = performance.now(); 30 | const responseCharacter = yield cacheIt('http://localhost:3000/graphql', { 31 | query: ` 32 | { 33 | people{ 34 | _id 35 | gender 36 | birth_year 37 | skin_color 38 | hair_color 39 | name 40 | species_id 41 | homeworld_id 42 | } 43 | }`, 44 | }, 10); 45 | console.log(responseCharacter); 46 | setCharacterinfo(responseCharacter.data.people); 47 | const endTime = performance.now(); 48 | setTime(endTime - startTime); 49 | }); 50 | /** 51 | * { 52 | reviews{ 53 | movie_id 54 | review 55 | } 56 | } 57 | */ 58 | const getReviews = () => __awaiter(void 0, void 0, void 0, function* () { 59 | const responseReview = yield cacheIt('http://localhost:3000/graphql', { 60 | query: ` 61 | { 62 | reviews { 63 | _id 64 | movie_id 65 | review 66 | } 67 | }`, 68 | }, 10); 69 | setReviewInfo(responseReview.data.reviews); 70 | }); 71 | const handleReview = (e) => { 72 | setReviewText(e.target.value); 73 | console.log(reviewText); 74 | }; 75 | const createReview = () => __awaiter(void 0, void 0, void 0, function* () { 76 | const post = yield cacheIt('http://localhost:3000/graphql', { 77 | mutation: `{ 78 | createReview(input: {movie_id: 4, text:"${reviewText}"}) { 79 | _id 80 | review 81 | } 82 | } 83 | ` 84 | }, 400); 85 | // --> variables 86 | }); 87 | // const getPeopleA = async () => { 88 | // const startTime = performance.now(); 89 | // const response: any = await fetch('http://localhost:3000/graphql', { 90 | // //Graphql Queries are performded as a post request 91 | // method: 'POST', 92 | // //The type of body being sent is an application/json 93 | // headers: { 94 | // 'Content-Type': 'application/json', 95 | // }, 96 | // //body of the response/request 97 | // body: JSON.stringify({ 98 | // query: ` 99 | // { 100 | // people{ 101 | // _id 102 | // gender 103 | // birth_year 104 | // skin_color 105 | // hair_color 106 | // name 107 | // species_id 108 | // homeworld_id 109 | // } 110 | // }`, 111 | // }), 112 | // }) 113 | // .then((res) => res.json()) 114 | // .then((data: any) => { 115 | // setCharacterinfo(data.data.people); 116 | // const endTime = performance.now(); 117 | // setTime(endTime - startTime); 118 | // }); 119 | // }; 120 | return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { onClick: getPeopleB, className: 'getPeople' }), (0, jsx_runtime_1.jsx)("button", { onClick: getReviews, className: 'getPeople' }), (0, jsx_runtime_1.jsx)("div", { className: 'hitmissbox', children: (0, jsx_runtime_1.jsx)(HitMiss_1.default, { time: time }) }), (0, jsx_runtime_1.jsx)("div", { className: 'cardBox', children: characterinfo.map((character) => ((0, jsx_runtime_1.jsx)(CharacterCard_1.default, { character: character }, character._id))) }), (0, jsx_runtime_1.jsx)("div", { className: 'reviewsBox', children: reviewInfo.map((review) => ((0, jsx_runtime_1.jsx)(ReviewCard_1.default, { review: review }, review._id))) }), (0, jsx_runtime_1.jsx)("input", { type: "text", placeholder: 'Type review here', onChange: handleReview }), (0, jsx_runtime_1.jsx)("button", { type: 'submit', onClick: createReview, children: "Submit Your Review" })] })); 121 | }; 122 | exports.default = Dashboard; 123 | -------------------------------------------------------------------------------- /test-client/distTSC/client/components/HitMiss.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const jsx_runtime_1 = require("react/jsx-runtime"); 4 | const HitMiss = ({ time }) => { 5 | return ((0, jsx_runtime_1.jsx)("div", { className: 'timebox', children: (0, jsx_runtime_1.jsx)("p", { children: time }) })); 6 | }; 7 | exports.default = HitMiss; 8 | -------------------------------------------------------------------------------- /test-client/distTSC/client/components/ReviewCard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ReviewCard = void 0; 4 | const jsx_runtime_1 = require("react/jsx-runtime"); 5 | const ReviewCard = ({ review }) => { 6 | return ((0, jsx_runtime_1.jsxs)("div", { className: 'ReviewBox', children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsx)("p", { children: review.review }), (0, jsx_runtime_1.jsx)("hr", {})] })); 7 | }; 8 | exports.ReviewCard = ReviewCard; 9 | exports.default = exports.ReviewCard; 10 | -------------------------------------------------------------------------------- /test-client/distTSC/client/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const jsx_runtime_1 = require("react/jsx-runtime"); 7 | const client_1 = require("react-dom/client"); 8 | const Dashboard_1 = __importDefault(require("./components/Dashboard")); 9 | require("./styles/characterCard.scss"); 10 | require("./styles/dashboard.scss"); 11 | require("./styles/time.scss"); 12 | const App = () => { 13 | return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Dashboard_1.default, {}) })); 14 | }; 15 | (0, client_1.createRoot)(document.querySelector('#root')).render((0, jsx_runtime_1.jsx)(App, {})); 16 | -------------------------------------------------------------------------------- /test-client/distTSC/client/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //you need to create a type for props or you will face errors 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | -------------------------------------------------------------------------------- /test-client/distTSC/server/models/starWarsModels.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Pool } = require('pg'); 3 | const PG_URI = 'postgresql://postgres.kxyqwoqggffsxsxzvwgz:$ChunkyChicken329@aws-0-us-east-2.pooler.supabase.com:6543/postgres'; 4 | // create a new pool here using the connection string above 5 | const pool = new Pool({ 6 | connectionString: PG_URI, 7 | }); 8 | // Adding some notes about the database here will be helpful for future you or other developers. 9 | // Schema for the database can be found below: 10 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png 11 | // We export an object that contains a property called query, 12 | // which is a function that returns the invocation of pool.query() after logging the query 13 | // This will be required in the controllers to be the access point to the database 14 | module.exports = { 15 | query: (text, params, callback) => { 16 | //console.log('executed query', text); 17 | return pool.query(text, params, callback); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test-client/distTSC/server/schema/resolvers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const db = require('../models/starWarsModels'); 13 | const schema = require('./schema'); 14 | //specifies the data types and Queries 15 | //an object that contains resolver functions 16 | //the keys correspond to the field names in the Query type 17 | //the values are functions that resolve the field's value 18 | module.exports = { 19 | //using postgre sql this resolver selects all of the people from the database and returns them 20 | //Query: { 21 | people: () => __awaiter(void 0, void 0, void 0, function* () { 22 | //query 23 | const query = 'SELECT * FROM people'; 24 | //request 25 | const results = yield db.query(query); 26 | //response being returned is in the shape of an array 27 | return results.rows; 28 | }), 29 | reviews: () => __awaiter(void 0, void 0, void 0, function* () { 30 | //query 31 | const query = 'SELECT * FROM reviews'; 32 | //request 33 | const results = yield db.query(query); 34 | //response being returned is in the shape of an array 35 | return results.rows; 36 | }), 37 | //This Resolver selects a single person from the people table 38 | person: (args) => __awaiter(void 0, void 0, void 0, function* () { 39 | //selects where the id matches 40 | const query = 'SELECT * FROM people WHERE _id = $1'; 41 | //grabs the id property off the args object 42 | const { id } = args; 43 | console.log('Args argument:', args); 44 | console.log('Extracted ID:', id); 45 | //queries the database 46 | const results = yield db.query(query, [id]); 47 | //console.log(args); 48 | //console.log(id); 49 | //returns the results in the proper format 50 | return results.rows[0]; 51 | }), 52 | // }, 53 | //Mutation: { 54 | createReview: ( 55 | //_parent: any, 56 | args) => __awaiter(void 0, void 0, void 0, function* () { 57 | const query = 'INSERT into reviews (movie_id,review) VALUES ($1,$2)'; 58 | const { movie_id, text } = args.input; 59 | //console.log(movie_id, text); 60 | const results = yield db.query(query, [movie_id, text]); 61 | console.log(results); 62 | return results.rows[0]; 63 | }), 64 | //}, 65 | }; 66 | -------------------------------------------------------------------------------- /test-client/distTSC/server/schema/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { buildSchema } = require('graphql'); 3 | /**Similar to how typescript makes you define the types to use the data 4 | * the schema makes you format how your data is coming so you can parse it how you want 5 | * within the query 6 | */ 7 | module.exports = buildSchema(` 8 | type person{ 9 | _id:ID! 10 | name:String! 11 | mass:String 12 | hair_color:String 13 | skin_color:String 14 | eye_color:String 15 | birth_year:String 16 | gender:String 17 | species_id:Int 18 | homeworld_id:Int 19 | height:Int 20 | } 21 | 22 | type movie{ 23 | _id: ID! 24 | title: String! 25 | episode: Int! 26 | opening_crawl: String! 27 | director: String! 28 | producer: String! 29 | release_date:Int! 30 | } 31 | 32 | type review{ 33 | _id:ID! 34 | movie_id: Int! 35 | review:String! 36 | } 37 | 38 | input ReviewInput{ 39 | text:String! 40 | movie_id: Int! 41 | } 42 | 43 | type Query { 44 | reviews: [review] 45 | people: [person] 46 | person(id: ID!): person! 47 | } 48 | 49 | type Mutation{ 50 | createReview(input: ReviewInput!): review 51 | } 52 | `); 53 | -------------------------------------------------------------------------------- /test-client/distTSC/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); 3 | const { graphqlHTTP } = require('express-graphql'); 4 | const cors = require('cors'); 5 | //const { buildSchema } = require('graphql'); 6 | //keep as require call to avoid err 7 | //const db = require('./models/starWarsModels'); 8 | const graphqlSchema = require('./schema/schema'); 9 | const rootValue = require('./schema/resolvers'); 10 | //the names of schema and rootValue matter, they must be named the exact same way 11 | const app = express(); 12 | app.use(cors()); 13 | app.use('/graphql', graphqlHTTP({ 14 | schema: graphqlSchema, 15 | rootValue, 16 | graphiql: true, 17 | })); 18 | app.listen(3000, () => console.log('listening on 3000')); 19 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/components/CharacterCard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const jsx_runtime_1 = require("react/jsx-runtime"); 4 | const CharacterCard = ({ character }) => { 5 | return ((0, jsx_runtime_1.jsxs)("div", { className: 'characterCard', children: [(0, jsx_runtime_1.jsx)("h1", { children: character.name }), (0, jsx_runtime_1.jsxs)("p", { children: ["Gender: ", character.gender] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Hair Color: ", character.hair_color] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Birth Year: ", character.birth_year] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Homeplanet: ", character.homeworld_id] }), (0, jsx_runtime_1.jsxs)("p", { children: ["Species: ", character.species_id] })] })); 6 | }; 7 | exports.default = CharacterCard; 8 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const jsx_runtime_1 = require("react/jsx-runtime"); 16 | const CharacterCard_1 = __importDefault(require("./CharacterCard")); 17 | const ReviewCard_1 = __importDefault(require("./ReviewCard")); 18 | const react_1 = require("react"); 19 | const HitMiss_1 = __importDefault(require("./HitMiss")); 20 | const { cacheIt } = require('../../../../cacheiql-client/dist/index'); 21 | const Dashboard = () => { 22 | const peopleArray = []; 23 | const reviewArray = []; 24 | const [characterinfo, setCharacterinfo] = (0, react_1.useState)(peopleArray); 25 | const [reviewInfo, setReviewInfo] = (0, react_1.useState)(reviewArray); 26 | const [reviewText, setReviewText] = (0, react_1.useState)(''); 27 | const [time, setTime] = (0, react_1.useState)(0); 28 | const getPeopleB = () => __awaiter(void 0, void 0, void 0, function* () { 29 | const startTime = performance.now(); 30 | const responseCharacter = yield cacheIt({ 31 | endpoint: 'http://localhost:3000/graphql', 32 | query: ` 33 | { 34 | people{ 35 | _id 36 | gender 37 | birth_year 38 | skin_color 39 | hair_color 40 | name 41 | species_id 42 | homeworld_id 43 | } 44 | }`, 45 | time: 3600, 46 | }); 47 | //console.log(responseCharacter); 48 | setCharacterinfo(responseCharacter.data.people); 49 | const endTime = performance.now(); 50 | setTime(endTime - startTime); 51 | }); 52 | const getReviews = () => __awaiter(void 0, void 0, void 0, function* () { 53 | const responseReview = yield cacheIt({ 54 | endpoint: 'http://localhost:3000/graphql', 55 | query: ` 56 | { 57 | reviews { 58 | _id 59 | movie_id 60 | review 61 | } 62 | }`, 63 | time: 3600, 64 | }); 65 | setReviewInfo(responseReview.data.reviews); 66 | }); 67 | const handleReview = (e) => { 68 | setReviewText(e.target.value); 69 | //console.log(reviewText); 70 | }; 71 | const createReview = () => __awaiter(void 0, void 0, void 0, function* () { 72 | const post = yield cacheIt({ 73 | endpoint: 'http://localhost:3000/graphql', 74 | mutation: `{ 75 | createReview(input: {movie_id: 4, text:"${reviewText}"}) { 76 | review 77 | } 78 | } 79 | `, 80 | time: 3600, 81 | }); 82 | // --> variables 83 | }); 84 | // const getPeopleA = async () => { 85 | // const startTime = performance.now(); 86 | // const response: any = await fetch('http://localhost:3000/graphql', { 87 | // //Graphql Queries are performded as a post request 88 | // method: 'POST', 89 | // //The type of body being sent is an application/json 90 | // headers: { 91 | // 'Content-Type': 'application/json', 92 | // }, 93 | // //body of the response/request 94 | // body: JSON.stringify({ 95 | // query: ` 96 | // { 97 | // people{ 98 | // _id 99 | // gender 100 | // birth_year 101 | // skin_color 102 | // hair_color 103 | // name 104 | // species_id 105 | // homeworld_id 106 | // } 107 | // }`, 108 | // }), 109 | // }) 110 | // .then((res) => res.json()) 111 | // .then((data: any) => { 112 | // setCharacterinfo(data.data.people); 113 | // const endTime = performance.now(); 114 | // setTime(endTime - startTime); 115 | // }); 116 | // }; 117 | return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("button", { onClick: getPeopleB, className: 'getPeople' }), (0, jsx_runtime_1.jsx)("button", { onClick: getReviews, className: 'getPeople' }), (0, jsx_runtime_1.jsx)("div", { className: 'hitmissbox', children: (0, jsx_runtime_1.jsx)(HitMiss_1.default, { time: time }) }), (0, jsx_runtime_1.jsx)("div", { className: 'cardBox', children: characterinfo.map((character) => ((0, jsx_runtime_1.jsx)(CharacterCard_1.default, { character: character }, character._id))) }), (0, jsx_runtime_1.jsx)("div", { className: 'reviewsBox', children: reviewInfo.map((review) => ((0, jsx_runtime_1.jsx)(ReviewCard_1.default, { review: review }, review._id))) }), (0, jsx_runtime_1.jsx)("input", { type: 'text', placeholder: 'Type review here', onChange: handleReview }), (0, jsx_runtime_1.jsx)("button", { type: 'submit', onClick: createReview, children: "Submit Your Review" })] })); 118 | }; 119 | exports.default = Dashboard; 120 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/components/HitMiss.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const jsx_runtime_1 = require("react/jsx-runtime"); 4 | const HitMiss = ({ time }) => { 5 | return ((0, jsx_runtime_1.jsx)("div", { className: 'timebox', children: (0, jsx_runtime_1.jsx)("p", { children: time }) })); 6 | }; 7 | exports.default = HitMiss; 8 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/components/ReviewCard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ReviewCard = void 0; 4 | const jsx_runtime_1 = require("react/jsx-runtime"); 5 | const ReviewCard = ({ review }) => { 6 | return ((0, jsx_runtime_1.jsxs)("div", { className: 'ReviewBox', children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsx)("p", { children: review.review }), (0, jsx_runtime_1.jsx)("hr", {})] })); 7 | }; 8 | exports.ReviewCard = ReviewCard; 9 | exports.default = exports.ReviewCard; 10 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const jsx_runtime_1 = require("react/jsx-runtime"); 7 | const client_1 = require("react-dom/client"); 8 | const Dashboard_1 = __importDefault(require("./components/Dashboard")); 9 | require("./styles/characterCard.scss"); 10 | require("./styles/dashboard.scss"); 11 | require("./styles/time.scss"); 12 | const App = () => { 13 | return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Dashboard_1.default, {}) })); 14 | }; 15 | (0, client_1.createRoot)(document.querySelector('#root')).render((0, jsx_runtime_1.jsx)(App, {})); 16 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/client/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //you need to create a type for props or you will face errors 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/server/models/starWarsModels.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Pool } = require('pg'); 3 | const PG_URI = 'postgresql://postgres.kxyqwoqggffsxsxzvwgz:$ChunkyChicken329@aws-0-us-east-2.pooler.supabase.com:6543/postgres'; 4 | // create a new pool here using the connection string above 5 | const pool = new Pool({ 6 | connectionString: PG_URI, 7 | }); 8 | // Adding some notes about the database here will be helpful for future you or other developers. 9 | // Schema for the database can be found below: 10 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png 11 | // We export an object that contains a property called query, 12 | // which is a function that returns the invocation of pool.query() after logging the query 13 | // This will be required in the controllers to be the access point to the database 14 | module.exports = { 15 | query: (text, params, callback) => { 16 | //console.log('executed query', text); 17 | return pool.query(text, params, callback); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/server/schema/resolvers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const db = require('../models/starWarsModels'); 13 | const schema = require('./schema'); 14 | //specifies the data types and Queries 15 | //an object that contains resolver functions 16 | //the keys correspond to the field names in the Query type 17 | //the values are functions that resolve the field's value 18 | module.exports = { 19 | //using postgre sql this resolver selects all of the people from the database and returns them 20 | //Query: { 21 | people: () => __awaiter(void 0, void 0, void 0, function* () { 22 | //query 23 | const query = 'SELECT * FROM people'; 24 | //request 25 | const results = yield db.query(query); 26 | //response being returned is in the shape of an array 27 | return results.rows; 28 | }), 29 | reviews: () => __awaiter(void 0, void 0, void 0, function* () { 30 | //query 31 | const query = 'SELECT * FROM reviews'; 32 | //request 33 | const results = yield db.query(query); 34 | //response being returned is in the shape of an array 35 | return results.rows; 36 | }), 37 | //This Resolver selects a single person from the people table 38 | person: (args) => __awaiter(void 0, void 0, void 0, function* () { 39 | //selects where the id matches 40 | const query = 'SELECT * FROM people WHERE _id = $1'; 41 | //grabs the id property off the args object 42 | const { id } = args; 43 | console.log('Args argument:', args); 44 | console.log('Extracted ID:', id); 45 | //queries the database 46 | const results = yield db.query(query, [id]); 47 | //console.log(args); 48 | //console.log(id); 49 | //returns the results in the proper format 50 | return results.rows[0]; 51 | }), 52 | // }, 53 | //Mutation: { 54 | createReview: ( 55 | //_parent: any, 56 | args) => __awaiter(void 0, void 0, void 0, function* () { 57 | const query = 'INSERT into reviews (movie_id,review) VALUES ($1,$2)'; 58 | const { movie_id, text } = args.input; 59 | //console.log(movie_id, text); 60 | const results = yield db.query(query, [movie_id, text]); 61 | console.log(results); 62 | return results.rows[0]; 63 | }), 64 | //}, 65 | }; 66 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/server/schema/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { buildSchema } = require('graphql'); 3 | /**Similar to how typescript makes you define the types to use the data 4 | * the schema makes you format how your data is coming so you can parse it how you want 5 | * within the query 6 | */ 7 | module.exports = buildSchema(` 8 | type person{ 9 | _id:ID! 10 | name:String! 11 | mass:String 12 | hair_color:String 13 | skin_color:String 14 | eye_color:String 15 | birth_year:String 16 | gender:String 17 | species_id:Int 18 | homeworld_id:Int 19 | height:Int 20 | } 21 | 22 | type movie{ 23 | _id: ID! 24 | title: String! 25 | episode: Int! 26 | opening_crawl: String! 27 | director: String! 28 | producer: String! 29 | release_date:Int! 30 | } 31 | 32 | type review{ 33 | _id:ID! 34 | movie_id: Int! 35 | review:String! 36 | } 37 | 38 | input ReviewInput{ 39 | text:String! 40 | movie_id: Int! 41 | } 42 | 43 | type Query { 44 | reviews: [review] 45 | people: [person] 46 | person(id: ID!): person! 47 | } 48 | 49 | type Mutation{ 50 | createReview(input: ReviewInput!): review 51 | } 52 | `); 53 | -------------------------------------------------------------------------------- /test-client/distTSC/test-client/src/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); 3 | const { graphqlHTTP } = require('express-graphql'); 4 | const cors = require('cors'); 5 | //const { buildSchema } = require('graphql'); 6 | //keep as require call to avoid err 7 | //const db = require('./models/starWarsModels'); 8 | const graphqlSchema = require('./schema/schema'); 9 | const rootValue = require('./schema/resolvers'); 10 | //the names of schema and rootValue matter, they must be named the exact same way 11 | const app = express(); 12 | app.use(cors()); 13 | app.use('/graphql', graphqlHTTP({ 14 | schema: graphqlSchema, 15 | rootValue, 16 | graphiql: true, 17 | })); 18 | app.listen(3000, () => console.log('listening on 3000')); 19 | -------------------------------------------------------------------------------- /test-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-client", 3 | "version": "1.0.0", 4 | "main": "./distTSC/src/client/index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "webpack-dev-server", 8 | "serv": "nodemon ./src/server/server.ts", 9 | "build": "webpack" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "devDependencies": { 16 | "@types/react-dom": "^19.0.2", 17 | "nodemon": "^3.1.7", 18 | "sass": "^1.83.0", 19 | "sass-loader": "^16.0.4", 20 | "ts-loader": "^9.5.1", 21 | "tsc": "^2.0.4", 22 | "typescript": "^5.7.2", 23 | "webpack": "^5.97.1", 24 | "webpack-cli": "^5.1.4", 25 | "webpack-dev-server": "^5.1.0" 26 | }, 27 | "dependencies": { 28 | "@types/react": "^19.0.1", 29 | "cacheiql-client": "^1.0.1", 30 | "cors": "^2.8.5", 31 | "css-loader": "^7.1.2", 32 | "express": "^4.21.2", 33 | "express-graphql": "^0.12.0", 34 | "graphql": "^15.0.0", 35 | "html-webpack-plugin": "^5.6.3", 36 | "pg": "^8.13.1", 37 | "react": "^19.0.0", 38 | "react-dom": "^19.0.0", 39 | "style-loader": "^4.0.0", 40 | "ts-node": "^10.9.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test-client/src/client/components/CharacterCard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { CharacterProps } from '../types'; 3 | 4 | const CharacterCard = ({ character }: CharacterProps) => { 5 | return ( 6 |
7 |

{character.name}

8 |

Gender: {character.gender}

9 |

Hair Color: {character.hair_color}

10 |

Birth Year: {character.birth_year}

11 |

Homeplanet: {character.homeworld_id}

12 |

Species: {character.species_id}

13 |
14 | ); 15 | }; 16 | 17 | export default CharacterCard; 18 | -------------------------------------------------------------------------------- /test-client/src/client/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import CharacterCard from './CharacterCard'; 2 | import ReviewCard from './ReviewCard'; 3 | import { useState, useEffect } from 'react'; 4 | import HitMiss from './HitMiss'; 5 | const { cacheIt } = require('../../../../cacheiql-client/dist/index'); 6 | import { ResponseObject } from '../types'; 7 | const Dashboard = () => { 8 | const peopleArray: Array = []; 9 | const reviewArray: Array = []; 10 | const [characterinfo, setCharacterinfo] = useState(peopleArray); 11 | const [reviewInfo, setReviewInfo] = useState(reviewArray); 12 | const [reviewText, setReviewText] = useState(''); 13 | const [time, setTime] = useState(0); 14 | const getPeopleB = async () => { 15 | setReviewInfo([]); 16 | setCharacterinfo([]); 17 | const startTime: number = performance.now(); 18 | const responseCharacter: ResponseObject = await cacheIt({ 19 | endpoint: 'http://localhost:3000/graphql', 20 | query: ` 21 | { 22 | people{ 23 | _id 24 | gender 25 | birth_year 26 | skin_color 27 | hair_color 28 | name 29 | species_id 30 | homeworld_id 31 | } 32 | }`, 33 | time: 3600, 34 | }); 35 | //console.log(responseCharacter); 36 | setCharacterinfo(responseCharacter.data.people); 37 | const endTime: number = performance.now(); 38 | setTime(endTime - startTime); 39 | }; 40 | 41 | const getReviews = async () => { 42 | setReviewInfo([]); 43 | setCharacterinfo([]); 44 | const startTime: number = performance.now(); 45 | const responseReview: ResponseObject = await cacheIt({ 46 | endpoint: 'http://localhost:3000/graphql', 47 | query: ` 48 | { 49 | reviews { 50 | _id 51 | movie_id 52 | review 53 | } 54 | }`, 55 | time: 3600, 56 | }); 57 | setReviewInfo(responseReview.data.reviews); 58 | const endTime: number = performance.now(); 59 | setTime(endTime - startTime); 60 | }; 61 | 62 | const handleReview = (e: any) => { 63 | setReviewText(e.target.value); 64 | //console.log(reviewText); 65 | }; 66 | 67 | const createReview = async () => { 68 | const post = await cacheIt({ 69 | endpoint: 'http://localhost:3000/graphql', 70 | mutation: `{ 71 | createReview(input: {movie_id: 4, text:"${reviewText}"}) { 72 | review 73 | } 74 | } 75 | `, 76 | time: 1, 77 | }); 78 | // --> variables 79 | }; 80 | 81 | // const getPeopleA = async () => { 82 | // const startTime = performance.now(); 83 | // const response: any = await fetch('http://localhost:3000/graphql', { 84 | // //Graphql Queries are performded as a post request 85 | // method: 'POST', 86 | // //The type of body being sent is an application/json 87 | // headers: { 88 | // 'Content-Type': 'application/json', 89 | // }, 90 | // //body of the response/request 91 | // body: JSON.stringify({ 92 | // query: ` 93 | 94 | // { 95 | // people{ 96 | // _id 97 | // gender 98 | // birth_year 99 | // skin_color 100 | // hair_color 101 | // name 102 | // species_id 103 | // homeworld_id 104 | // } 105 | // }`, 106 | // }), 107 | // }) 108 | // .then((res) => res.json()) 109 | // .then((data: any) => { 110 | // setCharacterinfo(data.data.people); 111 | 112 | // const endTime = performance.now(); 113 | // setTime(endTime - startTime); 114 | // }); 115 | // }; 116 | 117 | return ( 118 | <> 119 | 122 | 125 |
126 | 127 |
128 |
129 | {characterinfo.map((character: any) => ( 130 | 131 | ))} 132 |
133 | {/** populate a list of review cards based on a different request to gather all reviews*/} 134 |
135 | {reviewInfo.map((review: any) => ( 136 | 137 | ))} 138 |
139 | 145 | 148 | 149 | ); 150 | }; 151 | 152 | export default Dashboard; 153 | -------------------------------------------------------------------------------- /test-client/src/client/components/HitMiss.tsx: -------------------------------------------------------------------------------- 1 | import { HitMissProps } from '../types'; 2 | 3 | const HitMiss = ({ time }: HitMissProps) => { 4 | return ( 5 |
6 |

{time}

7 |
8 | ); 9 | }; 10 | 11 | export default HitMiss; 12 | -------------------------------------------------------------------------------- /test-client/src/client/components/ReviewCard.tsx: -------------------------------------------------------------------------------- 1 | import {ReviewCardProps} from '../types'; 2 | 3 | export const ReviewCard = ({review}: ReviewCardProps) =>{ 4 | return( 5 |
6 |
7 |

{review.review}

8 |
9 |
10 | ) 11 | } 12 | export default ReviewCard; -------------------------------------------------------------------------------- /test-client/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | Test-Client 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-client/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import Dashboard from './components/Dashboard'; 4 | import './styles/characterCard.scss'; 5 | import './styles/dashboard.scss'; 6 | import './styles/time.scss'; 7 | 8 | const App = () => { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | createRoot(document.querySelector('#root')!).render(); 17 | -------------------------------------------------------------------------------- /test-client/src/client/styles/characterCard.scss: -------------------------------------------------------------------------------- 1 | .characterCard { 2 | background-color: #2f2963; 3 | z-index: 1; 4 | width: 18rem; 5 | //border: 1px solid hsl(240, 61%, 92%); 6 | border-radius: 8px; 7 | box-shadow: 0px 0px #85c7f2; 8 | transition: transform 0.3s ease, box-shadow 0.3s ease; 9 | } 10 | 11 | .characterCard:hover { 12 | z-index: 5; 13 | transform: translateZ(-100px); 14 | transform: translateY(-10px); 15 | 16 | box-shadow: 15px 15px 10px #0b7189; 17 | } 18 | -------------------------------------------------------------------------------- /test-client/src/client/styles/dashboard.scss: -------------------------------------------------------------------------------- 1 | /* 2 | C19AB7 - Lilac for writing and buttons 3 | 0B7189 - Creulean TBD 4 | 85C7F2 - Light Sky blue TBD 5 | 170A1C - Dark Purple for background 6 | 2F2963 - Space Cadet not sure yet probably for highlights 7 | */ 8 | 9 | html { 10 | font-family: 'Source Code Pro', monospace; 11 | font-optical-sizing: auto; 12 | font-weight: 300; 13 | font-style: normal; 14 | color: #c19ab7; 15 | } 16 | 17 | body { 18 | height: 100vh; 19 | background-color: #170a1c; 20 | } 21 | 22 | button { 23 | font-family: 'Source Code Pro', monospace; 24 | font-optical-sizing: auto; 25 | font-weight: 300; 26 | font-style: normal; 27 | background-color: white; 28 | color: #c19ab7; 29 | border: none; 30 | transition: background-color 0.3s ease, color 0.3s ease, 31 | border-color 0.3s ease; 32 | cursor: pointer; 33 | } 34 | .inputBox { 35 | background-color: #2f2963; /* Space Cadet */ 36 | color: #c19ab7; /* Lilac */ 37 | border: 2px solid #c19ab7; 38 | padding: 0.75rem; 39 | font-family: 'Source Code Pro', monospace; 40 | font-size: 1rem; 41 | font-weight: 300; 42 | border-radius: 8px; 43 | outline: none; 44 | width: 100%; 45 | max-width: 400px; 46 | transition: border-color 0.3s ease, background-color 0.3s ease; 47 | } 48 | 49 | .inputBox::placeholder { 50 | color: #85c7f2; /* Light Sky Blue */ 51 | opacity: 0.7; 52 | } 53 | 54 | .inputBox:focus { 55 | border-color: #85c7f2; /* Light Sky Blue */ 56 | background-color: #0b7189; /* Cerulean */ 57 | color: white; 58 | } 59 | 60 | .cardBox { 61 | display: flex; 62 | flex-wrap: wrap; 63 | } 64 | .getPeople { 65 | padding: 30px; 66 | } 67 | 68 | h1 { 69 | font-family: Futura, 'Trebuchet MS', Arial, sans-serif; 70 | font-size: 24px; 71 | font-style: normal; 72 | font-variant: normal; 73 | font-weight: 700; 74 | line-height: 26.4px; 75 | } 76 | h3 { 77 | font-family: Futura, 'Trebuchet MS', Arial, sans-serif; 78 | font-size: 14px; 79 | font-style: normal; 80 | font-variant: normal; 81 | font-weight: 700; 82 | line-height: 15.4px; 83 | } 84 | .submit { 85 | background-color: white; /* Lilac */ 86 | color: #c19ab7; /* Dark Purple */ 87 | border: 2px solid #c19ab7; 88 | padding: 0.75rem 1.5rem; 89 | font-family: 'Source Code Pro', monospace; 90 | font-size: 1rem; 91 | font-weight: 300; 92 | border-radius: 8px; 93 | margin-left: 0.5rem; 94 | } 95 | 96 | button:hover { 97 | background-color: #85c7f2; /* Light Sky Blue */ 98 | color: #170a1c; 99 | border-color: #85c7f2; 100 | } 101 | 102 | button:active { 103 | background-color: #0b7189; /* Cerulean */ 104 | color: white; 105 | border-color: #0b7189; 106 | } 107 | 108 | .input-container { 109 | display: flex; 110 | align-items: center; 111 | gap: 0.5rem; 112 | } 113 | p { 114 | font-family: Futura, 'Trebuchet MS', Arial, sans-serif; 115 | font-size: 14px; 116 | font-style: normal; 117 | font-variant: normal; 118 | font-weight: 400; 119 | line-height: 20px; 120 | } 121 | blockquote { 122 | font-family: Futura, 'Trebuchet MS', Arial, sans-serif; 123 | font-size: 21px; 124 | font-style: normal; 125 | font-variant: normal; 126 | font-weight: 400; 127 | line-height: 30px; 128 | } 129 | -------------------------------------------------------------------------------- /test-client/src/client/styles/time.scss: -------------------------------------------------------------------------------- 1 | .timebox { 2 | position: fixed; /* Keeps it in place when scrolling */ 3 | top: 15px; /* Positions it near the top */ 4 | right: 15px; /* Positions it near the right */ 5 | background-color: white; 6 | padding: 0.75rem; 7 | border: 1px solid #170a1c; 8 | border-radius: 8px; 9 | z-index: 1000; /* Ensures it's above other elements */ 10 | } 11 | 12 | .hitmissbox { 13 | display: flex; 14 | justify-content: end; 15 | position: fixed; 16 | z-index: 10; 17 | } 18 | -------------------------------------------------------------------------------- /test-client/src/client/types.ts: -------------------------------------------------------------------------------- 1 | //you need to create a type for props or you will face errors 2 | 3 | export type CharacterProps = { 4 | character: { 5 | _id: string; 6 | name: string; 7 | skin_color: string; 8 | hair_color: string; 9 | eye_color: string; 10 | birth_year: string; 11 | species_id: number; 12 | gender: string; 13 | height: number; 14 | homeworld_id: number; 15 | }; 16 | }; 17 | 18 | export type HitMissProps = { 19 | time: number; 20 | }; 21 | 22 | export type ReviewCardProps = { 23 | review:{ 24 | _id: string; 25 | movie_id: number; 26 | review: string; 27 | } 28 | 29 | } 30 | 31 | 32 | export type ResponseObject = { 33 | data: { people: object[], reviews: object[]} 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /test-client/src/server/models/starWarsModels.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const PG_URI = 4 | 'postgresql://postgres.kxyqwoqggffsxsxzvwgz:$ChunkyChicken329@aws-0-us-east-2.pooler.supabase.com:6543/postgres'; 5 | 6 | // create a new pool here using the connection string above 7 | const pool = new Pool({ 8 | connectionString: PG_URI, 9 | }); 10 | 11 | // Adding some notes about the database here will be helpful for future you or other developers. 12 | // Schema for the database can be found below: 13 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png 14 | 15 | // We export an object that contains a property called query, 16 | // which is a function that returns the invocation of pool.query() after logging the query 17 | // This will be required in the controllers to be the access point to the database 18 | module.exports = { 19 | query: (text: string, params: any, callback: object) => { 20 | //console.log('executed query', text); 21 | return pool.query(text, params, callback); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /test-client/src/server/schema/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { query } from 'express'; 2 | 3 | const db = require('../models/starWarsModels'); 4 | const schema = require('./schema'); 5 | //specifies the data types and Queries 6 | //an object that contains resolver functions 7 | //the keys correspond to the field names in the Query type 8 | //the values are functions that resolve the field's value 9 | module.exports = { 10 | //using postgre sql this resolver selects all of the people from the database and returns them 11 | //Query: { 12 | people: async (): Promise => { 13 | //query 14 | const query = 'SELECT * FROM people'; 15 | //request 16 | const results = await db.query(query); 17 | //response being returned is in the shape of an array 18 | return results.rows; 19 | }, 20 | 21 | reviews: async (): Promise => { 22 | //query 23 | const query = 'SELECT * FROM reviews'; 24 | //request 25 | const results = await db.query(query); 26 | //response being returned is in the shape of an array 27 | return results.rows; 28 | }, 29 | 30 | //This Resolver selects a single person from the people table 31 | person: async (args: { id: string }): Promise => { 32 | //selects where the id matches 33 | const query = 'SELECT * FROM people WHERE _id = $1'; 34 | //grabs the id property off the args object 35 | const { id } = args; 36 | console.log('Args argument:', args); 37 | console.log('Extracted ID:', id); 38 | //queries the database 39 | const results = await db.query(query, [id]); 40 | //console.log(args); 41 | //console.log(id); 42 | //returns the results in the proper format 43 | return results.rows[0]; 44 | }, 45 | // }, 46 | 47 | //Mutation: { 48 | createReview: async ( 49 | //_parent: any, 50 | args: { input: { movie_id: number; text: string } } 51 | ): Promise => { 52 | const query = 'INSERT into reviews (movie_id,review) VALUES ($1,$2)'; 53 | const { movie_id, text } = args.input; 54 | //console.log(movie_id, text); 55 | const results = await db.query(query, [movie_id, text]); 56 | 57 | console.log(results); 58 | 59 | return results.rows[0]; 60 | }, 61 | //}, 62 | }; 63 | -------------------------------------------------------------------------------- /test-client/src/server/schema/schema.ts: -------------------------------------------------------------------------------- 1 | const { buildSchema } = require('graphql'); 2 | 3 | /**Similar to how typescript makes you define the types to use the data 4 | * the schema makes you format how your data is coming so you can parse it how you want 5 | * within the query 6 | */ 7 | module.exports = buildSchema(` 8 | type person{ 9 | _id:ID! 10 | name:String! 11 | mass:String 12 | hair_color:String 13 | skin_color:String 14 | eye_color:String 15 | birth_year:String 16 | gender:String 17 | species_id:Int 18 | homeworld_id:Int 19 | height:Int 20 | } 21 | 22 | type movie{ 23 | _id: ID! 24 | title: String! 25 | episode: Int! 26 | opening_crawl: String! 27 | director: String! 28 | producer: String! 29 | release_date:Int! 30 | } 31 | 32 | type review{ 33 | _id:ID! 34 | movie_id: Int! 35 | review:String! 36 | } 37 | 38 | input ReviewInput{ 39 | text:String! 40 | movie_id: Int! 41 | } 42 | 43 | type Query { 44 | reviews: [review] 45 | people: [person] 46 | person(id: ID!): person! 47 | } 48 | 49 | type Mutation{ 50 | createReview(input: ReviewInput!): review 51 | } 52 | `); 53 | -------------------------------------------------------------------------------- /test-client/src/server/server.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { graphqlHTTP } = require('express-graphql'); 3 | const cors = require('cors'); 4 | //const { buildSchema } = require('graphql'); 5 | //keep as require call to avoid err 6 | //const db = require('./models/starWarsModels'); 7 | const graphqlSchema = require('./schema/schema'); 8 | const rootValue = require('./schema/resolvers'); 9 | //the names of schema and rootValue matter, they must be named the exact same way 10 | 11 | const app = express(); 12 | 13 | app.use(cors()); 14 | app.use( 15 | '/graphql', 16 | graphqlHTTP({ 17 | schema: graphqlSchema, 18 | rootValue, 19 | graphiql: true, 20 | }) 21 | ); 22 | 23 | app.listen(3000, () => console.log('listening on 3000')); 24 | -------------------------------------------------------------------------------- /test-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ES6", 17 | "DOM" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 20 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "commonjs" /* Specify what module code is generated. */, 32 | // "rootDir": "./" /* Specify the root folder within your source files. */, 33 | "moduleResolution": "node10" /* Specify how TypeScript looks up a file from a given module specifier. */, 34 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 42 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 43 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 44 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 45 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 46 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 47 | // "resolveJsonModule": true, /* Enable importing .json files. */ 48 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 49 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 50 | 51 | /* JavaScript Support */ 52 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 53 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 54 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 55 | 56 | /* Emit */ 57 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 58 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 59 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 60 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 64 | "outDir": "./distTSC" /* Specify an output folder for all emitted files. */, 65 | // "removeComments": true, /* Disable emitting comments. */ 66 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 67 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 68 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 71 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 72 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 73 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 74 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 75 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 76 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 77 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 78 | 79 | /* Interop Constraints */ 80 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 81 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 82 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 83 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 84 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 85 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 86 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 87 | 88 | /* Type Checking */ 89 | "strict": true /* Enable all strict type-checking options. */, 90 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 91 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 92 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 93 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 94 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 95 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 96 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 97 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 98 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 99 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 100 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 101 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 102 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 103 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 104 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 105 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 106 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 107 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 108 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 109 | 110 | /* Completeness */ 111 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 112 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 113 | }, 114 | "include": [ 115 | "src", // Include your source files in test-client 116 | // "../cacheiql-client", // Include the dist folder of cacheiql-client 117 | "../cacheiql-client/dist" 118 | ], 119 | "exclude": [ 120 | "node_modules", 121 | "dist" 122 | ] 123 | 124 | } 125 | -------------------------------------------------------------------------------- /test-client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { default: test } = require('node:test'); 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/client/index.tsx', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'main.js', 10 | }, 11 | devServer: { 12 | port: '8081', 13 | static: { 14 | directory: path.join(__dirname, './dist'), 15 | }, 16 | open: true, 17 | hot: true, 18 | liveReload: true, 19 | }, 20 | 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(ts|tsx|jsx|js)$/, 25 | exclude: /node_modules/, 26 | use: ['ts-loader'], 27 | }, 28 | { 29 | test: /\.(scss|css)$/, 30 | use: ['style-loader', 'css-loader', 'sass-loader'], 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | template: './src/client/index.html', 40 | filename: './index.html', 41 | }), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /test-server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/test-server/.DS_Store -------------------------------------------------------------------------------- /test-server/distTSC/src/client/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const jsx_runtime_1 = require("react/jsx-runtime"); 4 | const client_1 = require("react-dom/client"); 5 | const App = () => { 6 | return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("h1", { children: "Client side Testing" }) })); 7 | }; 8 | (0, client_1.createRoot)(document.querySelector('#root')).render((0, jsx_runtime_1.jsx)(App, {})); 9 | -------------------------------------------------------------------------------- /test-server/distTSC/src/server/models/starWarsModels.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { Pool } = require('pg'); 3 | const PG_URI = 'postgresql://postgres.kxyqwoqggffsxsxzvwgz:$ChunkyChicken329@aws-0-us-east-2.pooler.supabase.com:5432/postgres'; 4 | // create a new pool here using the connection string above 5 | const pool = new Pool({ 6 | connectionString: PG_URI, 7 | }); 8 | // Adding some notes about the database here will be helpful for future you or other developers. 9 | // Schema for the database can be found below: 10 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png 11 | // We export an object that contains a property called query, 12 | // which is a function that returns the invocation of pool.query() after logging the query 13 | // This will be required in the controllers to be the access point to the database 14 | module.exports = { 15 | query: (text, params, callback) => { 16 | //console.log('executed query', text); 17 | return pool.query(text, params, callback); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test-server/distTSC/src/server/schema/resolvers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const db = require('../models/starWarsModels'); 13 | const schema = require('./schema'); 14 | //specifies the data types and Queries 15 | //an object that contains resolver functions 16 | //the keys correspond to the field names in the Query type 17 | //the values are functions that resolve the field's value 18 | module.exports = { 19 | //using postgre sql this resolver selects all of the people from the database and returns them 20 | //Query: { 21 | people: () => __awaiter(void 0, void 0, void 0, function* () { 22 | //query 23 | const query = 'SELECT * FROM people'; 24 | //request 25 | const results = yield db.query(query); 26 | //response being returned is in the shape of an array 27 | return results.rows; 28 | }), 29 | //This Resolver selects a single person from the people table 30 | person: (args) => __awaiter(void 0, void 0, void 0, function* () { 31 | //selects where the id matches 32 | const query = 'SELECT * FROM people WHERE _id = $1'; 33 | //grabs the id property off the args object 34 | const { id } = args; 35 | console.log('Args argument:', args); 36 | console.log('Extracted ID:', id); 37 | //queries the database 38 | const results = yield db.query(query, [id]); 39 | //console.log(args); 40 | //console.log(id); 41 | //returns the results in the proper format 42 | return results.rows[0]; 43 | }), 44 | // }, 45 | //Mutation: { 46 | createReview: ( 47 | //_parent: any, 48 | args) => __awaiter(void 0, void 0, void 0, function* () { 49 | const query = 'INSERT into reviews (movie_id,review) VALUES ($1,$2)'; 50 | const { movie_id, text } = args.input; 51 | const results = yield db.query(query, [movie_id, text]); 52 | return results.rows[0]; 53 | }), 54 | //}, 55 | }; 56 | -------------------------------------------------------------------------------- /test-server/distTSC/src/server/schema/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { buildSchema } = require('graphql'); 3 | /**Similar to how typescript makes you define the types to use the data 4 | * the schema makes you format how your data is coming so you can parse it how you want 5 | * within the query 6 | */ 7 | module.exports = buildSchema(` 8 | type person{ 9 | _id:ID! 10 | name:String! 11 | mass:String 12 | hair_color:String 13 | skin_color:String 14 | eye_color:String 15 | birth_year:String 16 | gender:String 17 | species_id:Int 18 | homeworld_id:Int 19 | height:Int 20 | } 21 | 22 | type movie{ 23 | _id: ID! 24 | title: String! 25 | episode: Int! 26 | opening_crawl: String! 27 | director: String! 28 | producer: String! 29 | release_date:Int! 30 | } 31 | 32 | type review{ 33 | _id:ID! 34 | movie_id: Int! 35 | review:String! 36 | } 37 | 38 | input ReviewInput{ 39 | text:String! 40 | movie_id: Int! 41 | } 42 | 43 | type Query { 44 | people: [person] 45 | person(id: ID!): person! 46 | } 47 | 48 | type Mutation{ 49 | createReview(input: ReviewInput!): review 50 | } 51 | `); 52 | -------------------------------------------------------------------------------- /test-server/distTSC/src/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const express = require('express'); 3 | const { graphqlHTTP } = require('express-graphql'); 4 | const { cacheMiddleware } = require('cacheiql-server'); 5 | //const { buildSchema } = require('graphql'); 6 | //keep as require call to avoid err 7 | //const db = require('./models/starWarsModels'); 8 | const graphqlSchema = require('./schema/schema'); 9 | const rootValue = require('./schema/resolvers'); 10 | //the names of schema and rootValue matter, they must be named the exact same way 11 | const app = express(); 12 | const testOBJ = { 13 | print: (string) => { 14 | return string; 15 | }, 16 | }; 17 | console.log('Root Values: ', rootValue); // Check if the resolvers are properly defined. 18 | app.use('/graphql', graphqlHTTP({ 19 | schema: graphqlSchema, 20 | //rootValue:rootValue, 21 | rootValue: Object.keys(rootValue).reduce((wrappedResolvers, key) => { 22 | console.log(`Wrapping resolver for ${key}`); 23 | const wrappedResolver = cacheMiddleware(rootValue[key]); 24 | // Logging to make sure we're wrapping the function correctly 25 | console.log(`Wrapped resolver for ${key}: `, wrappedResolver); 26 | return Object.assign(Object.assign({}, wrappedResolvers), { [key]: wrappedResolver }); 27 | }, {}), 28 | graphiql: true, 29 | })); 30 | app.listen(3000, () => console.log('listening on 3000')); 31 | console.log('middleware', cacheMiddleware); 32 | -------------------------------------------------------------------------------- /test-server/distTSC/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/client/index.tsx', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'main.js', 10 | }, 11 | devServer: { 12 | port: '8080', 13 | static: { 14 | directory: path.join(__dirname, './dist'), 15 | }, 16 | open: true, 17 | hot: true, 18 | liveReload: true, 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(ts|tsx|jsx|js)$/, 24 | exclude: /node_modules/, 25 | use: ['ts-loader'], 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 31 | }, 32 | plugins: [ 33 | new HtmlWebpackPlugin({ 34 | template: './src/client/index.html', 35 | filename: './index.html', 36 | }), 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /test-server/dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/test-server/dump.rdb -------------------------------------------------------------------------------- /test-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-client", 3 | "version": "1.0.0", 4 | "main": "./distTSC/src/client/index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "webpack-dev-server", 8 | "serv": "nodemon ./src/server/server.ts", 9 | "build": "webpack" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "devDependencies": { 16 | "@types/graphql": "^14.2.3", 17 | "@types/react": "^19.0.8", 18 | "@types/react-dom": "^19.0.3", 19 | "nodemon": "^3.1.7", 20 | "ts-loader": "^9.5.1", 21 | "tsc": "^2.0.4", 22 | "typescript": "^5.7.2", 23 | "webpack": "^5.97.1", 24 | "webpack-cli": "^5.1.4", 25 | "webpack-dev-server": "^5.1.0" 26 | }, 27 | "dependencies": { 28 | "express": "^4.21.2", 29 | "express-graphql": "^0.12.0", 30 | "html-webpack-plugin": "^5.6.3", 31 | "pg": "^8.13.1", 32 | "react": "^19.0.0", 33 | 34 | "react-dom": "^19.0.0" 35 | }, 36 | "peerDependencies": { 37 | "graphql": "^15.10.1" 38 | }, 39 | "resolutions": { 40 | "graphql": "^15.10.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test-server/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/test-server/src/.DS_Store -------------------------------------------------------------------------------- /test-server/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test-Client 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test-server/src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | const App = () => { 5 | return ( 6 |
7 |

Client side Testing

8 |
9 | ); 10 | }; 11 | 12 | createRoot(document.querySelector('#root')!).render(); 13 | -------------------------------------------------------------------------------- /test-server/src/server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/CacheIQL/6a3429a09c89f66dc7f5c496bbd0a1dd316ec259/test-server/src/server/.DS_Store -------------------------------------------------------------------------------- /test-server/src/server/models/starWarsModels.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const PG_URI = 4 | 'postgresql://postgres.kxyqwoqggffsxsxzvwgz:$ChunkyChicken329@aws-0-us-east-2.pooler.supabase.com:5432/postgres'; 5 | 6 | // create a new pool here using the connection string above 7 | const pool = new Pool({ 8 | connectionString: PG_URI, 9 | }); 10 | 11 | // Adding some notes about the database here will be helpful for future you or other developers. 12 | // Schema for the database can be found below: 13 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png 14 | 15 | // We export an object that contains a property called query, 16 | // which is a function that returns the invocation of pool.query() after logging the query 17 | // This will be required in the controllers to be the access point to the database 18 | module.exports = { 19 | query: (text: string, params: any, callback: object) => { 20 | //console.log('executed query', text); 21 | return pool.query(text, params, callback); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /test-server/src/server/schema/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { query } from 'express'; 2 | 3 | const db = require('../models/starWarsModels'); 4 | const schema = require('./schema'); 5 | module.exports = { 6 | people: async (_parent: any, _args: any, _context: any) => { 7 | const query = 'SELECT * FROM people'; 8 | const results = await db.query(query); 9 | return results.rows; 10 | }, 11 | 12 | 13 | 14 | // This Resolver selects a single person from the people table 15 | person: async (args: { id: string }): Promise => { 16 | //selects where the id matches 17 | const query = 'SELECT * FROM people WHERE _id = $1'; 18 | //grabs the id property off the args object 19 | const { id } = args; 20 | console.log('Args argument:', args); 21 | console.log('Extracted ID:', id); 22 | //queries the database 23 | const results = await db.query(query, [id]); 24 | return results.rows[0]; 25 | }, 26 | 27 | createReview: async ( 28 | args: { input: { movie_id: number; text: string } } 29 | ): Promise => { 30 | const query = 'INSERT into reviews (movie_id,review) VALUES ($1,$2) RETURNING *'; 31 | const { movie_id, text } = args.input; 32 | const results = await db.query(query, [movie_id, text]); 33 | 34 | return results.rows[0]; 35 | }, 36 | createPerson: async (args: { 37 | input: { 38 | name: string; 39 | mass?: string; 40 | hair_color?: string; 41 | skin_color?: string; 42 | eye_color?: string; 43 | birth_year?: string; 44 | gender?: string; 45 | species_id?: number; 46 | homeworld_id?: number; 47 | height?: number; 48 | }; 49 | }) => { 50 | const query = ` 51 | INSERT INTO people (name, mass, hair_color, skin_color, eye_color, birth_year, gender, species_id, homeworld_id, height) 52 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) 53 | RETURNING _id, name, species_id, homeworld_id`; 54 | 55 | const { 56 | name, 57 | mass, 58 | hair_color, 59 | skin_color, 60 | eye_color, 61 | birth_year, 62 | gender, 63 | species_id, 64 | homeworld_id, 65 | height, 66 | } = args.input; 67 | 68 | const results = await db.query(query, [ 69 | name, 70 | mass, 71 | hair_color, 72 | skin_color, 73 | eye_color, 74 | birth_year, 75 | gender, 76 | species_id, 77 | homeworld_id, 78 | height, 79 | ]); 80 | console.log('args', args); 81 | // ✅ Invalidate the cache so new data is fetched 82 | // await invalidateCacheForMutation('createPerson', args); 83 | 84 | const newPerson = results.rows[0]; 85 | 86 | console.log('✅ Created person:', newPerson); 87 | 88 | // ✅ Invalidate cache using the actual person ID 89 | // await invalidateCacheForMutation('createPerson', newPerson); 90 | 91 | return newPerson; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /test-server/src/server/schema/schema.ts: -------------------------------------------------------------------------------- 1 | const { buildSchema } = require('graphql'); 2 | 3 | /**Similar to how typescript makes you define the types to use the data 4 | * the schema makes you format how your data is coming so you can parse it how you want 5 | * within the query 6 | */ 7 | module.exports = buildSchema(` 8 | type Person{ 9 | _id:ID! 10 | name:String! 11 | mass:String 12 | hair_color:String 13 | skin_color:String 14 | eye_color:String 15 | birth_year:String 16 | gender:String 17 | species_id:Int 18 | homeworld_id:Int 19 | height:Int 20 | } 21 | 22 | type Movie{ 23 | _id: ID! 24 | title: String! 25 | episode: Int! 26 | opening_crawl: String! 27 | director: String! 28 | producer: String! 29 | release_date:Int! 30 | } 31 | 32 | type Review{ 33 | _id:ID! 34 | movie_id: Int! 35 | review:String! 36 | } 37 | 38 | input ReviewInput{ 39 | text:String! 40 | movie_id: Int! 41 | fettyWap: String! 42 | } 43 | 44 | input PersonInput { 45 | name: String! 46 | mass: String 47 | hair_color: String 48 | skin_color: String 49 | eye_color: String 50 | birth_year: String 51 | gender: String 52 | species_id: Int 53 | homeworld_id: Int 54 | height: Int 55 | } 56 | type Query { 57 | 58 | people: [person] 59 | person(id: ID!): person! 60 | reviews: [review] 61 | 62 | } 63 | 64 | type Mutation{ 65 | createReview(input: ReviewInput!): Review 66 | createPerson(input: PersonInput!): Person 67 | updatePerson(id: ID!, input: PersonInput!): Person 68 | } 69 | `); 70 | -------------------------------------------------------------------------------- /test-server/src/server/server.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { graphqlHTTP } = require('express-graphql'); 3 | const { cacheMiddleware } = require("../../../cacheiql-server/src/middleware/cacheMiddleware.ts"); 4 | 5 | const graphqlSchema = require('./schema/schema'); 6 | const rootValue = require('./schema/resolvers'); 7 | 8 | 9 | const app = express(); 10 | 11 | 12 | 13 | const TTL_IN_SECONDS = 1000; 14 | 15 | app.use( 16 | "/graphql", 17 | graphqlHTTP({ 18 | schema: graphqlSchema, 19 | //rootValue:rootValue, 20 | 21 | rootValue: cacheMiddleware(rootValue, TTL_IN_SECONDS, graphqlSchema), 22 | 23 | graphiql: true, 24 | }) 25 | ); 26 | 27 | app.listen(3000, () => console.log('listening on 3000')); 28 | 29 | 30 | -------------------------------------------------------------------------------- /test-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ES6", 17 | "DOM" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 20 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "commonjs" /* Specify what module code is generated. */, 32 | "rootDir": "./" /* Specify the root folder within your source files. */, 33 | "moduleResolution": "node10" /* Specify how TypeScript looks up a file from a given module specifier. */, 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 42 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 43 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 44 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 45 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 46 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 47 | // "resolveJsonModule": true, /* Enable importing .json files. */ 48 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 49 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 50 | 51 | /* JavaScript Support */ 52 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 53 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 54 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 55 | 56 | /* Emit */ 57 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 58 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 59 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 60 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 61 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 64 | "outDir": "./distTSC" /* Specify an output folder for all emitted files. */, 65 | // "removeComments": true, /* Disable emitting comments. */ 66 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 67 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 68 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 71 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 72 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 73 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 74 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 75 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 76 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 77 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 78 | 79 | /* Interop Constraints */ 80 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 81 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 82 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 83 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 84 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 85 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 86 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 87 | 88 | /* Type Checking */ 89 | "strict": true /* Enable all strict type-checking options. */, 90 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 91 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 92 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 93 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 94 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 95 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 96 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 97 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 98 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 99 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 100 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 101 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 102 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 103 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 104 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 105 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 106 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 107 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 108 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 109 | 110 | /* Completeness */ 111 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 112 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test-server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/client/index.tsx', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'main.js', 9 | }, 10 | devServer: { 11 | port: '8080', 12 | static: { 13 | directory: path.join(__dirname, './dist'), 14 | }, 15 | open: true, 16 | hot: true, 17 | liveReload: true, 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(ts|tsx|jsx|js)$/, 24 | exclude: /node_modules/, 25 | use: ['ts-loader'], 26 | }, 27 | ], 28 | }, 29 | resolve: { 30 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 31 | }, 32 | plugins: [ 33 | new HtmlWebpackPlugin({ 34 | template: './src/client/index.html', 35 | filename: './index.html', 36 | }), 37 | ], 38 | }; 39 | --------------------------------------------------------------------------------