├── .DS_Store
├── .d.ts
├── .eslintrc
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── __tests__
├── IntrospectionQuery.js
├── localStorageMock.js
├── magnicache-client.test.ts
├── magnicache-demo.test.ts
└── magnicache-server.test.ts
├── assets
├── OGLOGO.png
├── Readme-graph.jpg
├── RoundLogo.png
└── readmegif.gif
├── client
├── .d.ts
├── App.tsx
├── assets
│ ├── Logo.png
│ ├── ahm.png
│ ├── aria.png
│ ├── tru.png
│ └── you.png
├── components
│ ├── CacheMetrics.tsx
│ ├── NavBar.tsx
│ ├── QueryDisplay.tsx
│ ├── Response.tsx
│ ├── Result.tsx
│ └── VisualsDisplay.tsx
├── containers
│ ├── AboutPage.tsx
│ ├── DocsPage.tsx
│ ├── MetricsContainer.tsx
│ ├── TeamPage.tsx
│ └── XMetrics.tsx
├── index.html
├── index.tsx
├── magnicache-client.js
├── package-lock.json
├── package.json
└── scss
│ └── styles.scss
├── jest.config.js
├── magnicache-client
├── README.md
├── magnicache-client.js
├── magnicache-client.ts
├── package-lock.json
└── package.json
├── magnicache-demo
├── db-model.js
├── magnicache-server
│ ├── IntrospectionQuery.js
│ └── magnicache-server.js
├── package-lock.json
├── package.json
├── server.js
└── types.js
├── magnicache-server
├── IntrospectionQuery.js
├── README.md
├── db
│ └── index.js
├── magnicache-server.js
├── magnicache-server.ts
├── mutationAST.json
├── package-lock.json
├── package.json
├── schema.js
├── schema.json
└── test.js
├── netlify.toml
├── package-lock.json
├── package.json
├── tsconfig.json
├── types.js
├── types.ts
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/.DS_Store
--------------------------------------------------------------------------------
/.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-dom';
2 | declare module 'react-dom/client';
3 | declare module '*.png';
4 | declare module 'lodash.mergewith';
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "root": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Build folder
107 | build
108 |
109 | .ds_store
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:latest
2 | WORKDIR /usr/src/app
3 | # COPY . ./package.json ./package-lock.json ./magnicache-demo/package.json ./magnicache-server/package.json ./client/package.json /app/
4 | # COPY ./magnicache-client/package.json ./magnicache-client/package-lock.json /app/magnicache-client/
5 | COPY . /usr/src/app
6 | RUN npm install
7 | RUN cd client && npm install
8 | # RUN cd magnicache-client && npm install
9 | RUN cd magnicache-demo && npm install
10 | # RUN cd magnicache-server && npm install
11 | RUN npm run build
12 | EXPOSE 3000
13 | ENTRYPOINT [ "node", "./magnicache-demo/server.js" ]
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MagniCache
2 |
3 |
4 |
5 |
6 |
7 | ## What is MagniCache?
8 |
9 | MagniCache is a lightweight and performant GraphQL caching solution. Packaged and shipped as two separate npm packages, MagniCache can be implemented seamlessly into projects as an Express middleware for server-side caching of queries and mutations, or as a localStorage manipulation device for client-side caching.
10 |
11 | ## Why use MagniCache?
12 |
13 | MagniCache is unique in its exceptional efficiency. MagniCache's caching algorithm methodically parses GraphQL queries and divides them into atomic components, allowing for systematic and fully coherent caching of queries and improvements in performance. Unlike other GraphQL caching layers, subsequent GraphQL queries do not have to be exact duplicates of cached queries in order to benefit from cached response speeds. In addition, MagniCache was developed with compactness as the priority, so you can rest assured that implementing MagniCache into your projects will add zero unnecessary bulk.
14 |
15 |
16 |
17 |
Caching of queries leads to nearly instantaneous response times!
18 |
19 |
20 | ## How to use MagniCache
21 |
22 | Click here to demo MagniCache!
23 |
24 | Type your GraphQL queries in the Query field on the left and click Run. Check out the query response and metrics below to observe caching in action!
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## Installing MagniCache
33 |
34 | ### Server-Side Caching
35 |
36 | ```bash
37 | npm i @magnicache/server
38 | ```
39 |
40 | ### Client-Side Caching
41 |
42 | ```bash
43 | npm i @magnicache/client
44 | ```
45 |
46 | ## Documentation
47 |
48 | After deciding on how to implement MagniCache into your project, follow the links below for detailed installation steps.
49 |
50 |
54 |
55 | ## How to Contribute
56 |
57 | As an Open Source Product, we are always welcoming contributions! To be apart of MagniCache, you can follow the steps below:
58 |
59 | 1. Fork this repository, copying the `dev` branch.
60 |
61 | 2. Create your feature branch from `dev`.
62 |
63 | 3. Once you have finished contributing to your feature branch, add and commit all changes.
64 |
65 | 4. Locally merge your branch with with the `dev` branch.
66 |
67 | 5. Push your branch to GitHub and open a pull request.
68 |
69 | ## License
70 |
71 | MIT
72 |
73 | ## Contributors
74 |
75 | Ahmed Chami / Github / LinkedIn
76 |
77 | Aria Soltankhah / Github / LinkedIn
78 |
79 | Truman Miller / Github / LinkedIn
80 |
81 | Yousuf Elkhoga / Github / LinkedIn
82 |
--------------------------------------------------------------------------------
/__tests__/IntrospectionQuery.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | IntrospectionQuery: `query IntrospectionQuery {
3 | __schema {
4 |
5 | queryType { name fields{name type{...TypeRef}} }
6 | mutationType { name fields{name type{...TypeRef}} }
7 | types {
8 | ...FullType
9 | }
10 | }
11 | }
12 |
13 | fragment FullType on __Type {
14 | kind
15 | name
16 | description
17 |
18 | fields(includeDeprecated: true) {
19 | name
20 | description
21 | args {
22 | ...InputValue
23 | }
24 | type {
25 | ...TypeRef
26 | }
27 | isDeprecated
28 | deprecationReason
29 | }
30 | inputFields {
31 | ...InputValue
32 | }
33 | interfaces {
34 | ...TypeRef
35 | }
36 | enumValues(includeDeprecated: true) {
37 | name
38 | description
39 | isDeprecated
40 | deprecationReason
41 | }
42 | possibleTypes {
43 | ...TypeRef
44 | }
45 | }
46 |
47 | fragment InputValue on __InputValue {
48 | name
49 | description
50 | type { ...TypeRef }
51 | defaultValue
52 |
53 |
54 | }
55 |
56 | fragment TypeRef on __Type {
57 | kind
58 | name
59 | ofType {
60 | kind
61 | name
62 | ofType {
63 | kind
64 | name
65 | ofType {
66 | kind
67 | name
68 | ofType {
69 | kind
70 | name
71 | ofType {
72 | kind
73 | name
74 | ofType {
75 | kind
76 | name
77 | ofType {
78 | kind
79 | name
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }`,
88 | };
89 |
--------------------------------------------------------------------------------
/__tests__/localStorageMock.js:
--------------------------------------------------------------------------------
1 | class LocalStorageMock {
2 | constructor() {
3 | this.store = {};
4 | }
5 |
6 | clear() {
7 | this.store = {};
8 | }
9 |
10 | getItem(key) {
11 | return this.store[key] || null;
12 | }
13 |
14 | setItem(key, value) {
15 | this.store[key] = String(value);
16 | }
17 |
18 | removeItem(key) {
19 | delete this.store[key];
20 | }
21 | }
22 |
23 | module.exports = LocalStorageMock;
24 |
--------------------------------------------------------------------------------
/__tests__/magnicache-client.test.ts:
--------------------------------------------------------------------------------
1 | const MagniClient = require('../magnicache-client/magnicache-client.ts');
2 | const LocalStorageMock = require('./localStorageMock.js');
3 | global.localStorage = new LocalStorageMock();
4 |
5 | describe('MagniClient', () => {
6 | let magniClient: any;
7 |
8 | beforeEach(() => {
9 | localStorage.clear();
10 | magniClient = new MagniClient(2);
11 | });
12 |
13 | describe('constructor', () => {
14 | it('should instantiate the cache with the provided maxSize or default', () => {
15 | expect(magniClient.maxSize).toBe(2);
16 | });
17 |
18 | it('should initialize the cache from localStorage, if available', () => {
19 | localStorage.setItem('MagniClient', JSON.stringify(['query1', 'query2']));
20 |
21 | const newMagniClient = new MagniClient();
22 | expect(newMagniClient.cache).toEqual(['query1', 'query2']);
23 | });
24 |
25 | it('should not add duplicates to the cache', () => {
26 | localStorage.setItem(
27 | 'MagniClient',
28 | JSON.stringify(['query1', 'query2', 'query2'])
29 | );
30 | const newMagniClient = new MagniClient();
31 |
32 | expect(newMagniClient.cache).toEqual(['query1', 'query2']);
33 | });
34 |
35 | // TODO: test for MagniClient's methods
36 | });
37 |
38 | describe('set', () => {
39 | it('should store the provided query and value in localStorage', () => {
40 | magniClient.set('query1', { data: 'some data' });
41 |
42 | expect(localStorage.getItem('query1')).toBe('{"data":"some data"}');
43 | });
44 |
45 | it('should add the query to the cache', () => {
46 | magniClient.set('query1', { data: 'some data' });
47 |
48 | expect(magniClient.cache).toEqual(['query1']);
49 | });
50 |
51 | it('should remove the least recently used query from the cache if the cache is at maxSize', () => {
52 | magniClient.set('query1', { data: 'some data' });
53 | magniClient.set('query2', { data: 'some more data' });
54 | magniClient.set('query3', { data: 'even more data' });
55 |
56 | expect(magniClient.cache).toEqual(['query2', 'query3']);
57 | expect(localStorage.getItem('query1')).toBe(null);
58 | });
59 | });
60 |
61 | describe('get', () => {
62 | it('should return the value stored in localStorage for the provided query', () => {
63 | localStorage.setItem('query1', '{"data":"some data"}');
64 |
65 | expect(magniClient.get('query1')).toEqual({ data: 'some data' });
66 | });
67 |
68 | it('should move the query to the back of the cache', () => {
69 | magniClient.set('query1', { data: 'some data' });
70 | magniClient.set('query2', { data: 'some more data' });
71 |
72 | magniClient.get('query1');
73 | expect(magniClient.cache).toEqual(['query2', 'query1']);
74 | });
75 |
76 | it('should return an empty object if the query is not in localStorage', () => {
77 | expect(magniClient.get('query1')).toEqual({});
78 | });
79 | });
80 | });
81 |
82 | describe('MagniClient.magniParser', () => {
83 | let magniClient: any;
84 |
85 | beforeAll(() => {
86 | magniClient = new MagniClient();
87 | });
88 |
89 | it('should parse single selection without arguments', () => {
90 | const selections = [
91 | {
92 | kind: 'Field',
93 | name: {
94 | kind: 'Name',
95 | value: 'allMessages',
96 | },
97 | arguments: [],
98 | },
99 | ];
100 |
101 | const result = magniClient.magniParser(selections);
102 | expect(result).toEqual(['{allMessages}']);
103 | });
104 |
105 | it('should parse single selection with arguments', () => {
106 | const selections = [
107 | {
108 | kind: 'Field',
109 | name: {
110 | kind: 'Name',
111 | value: 'messageById',
112 | },
113 | arguments: [
114 | {
115 | kind: 'Argument',
116 | name: {
117 | kind: 'Name',
118 | value: 'id',
119 | },
120 | value: {
121 | kind: 'StringValue',
122 | value: '4',
123 | },
124 | },
125 | ],
126 | },
127 | ];
128 |
129 | const result = magniClient.magniParser(selections);
130 | expect(result).toEqual(['{messageById(id:4)}']);
131 | });
132 |
133 | it('should parse nested selections without arguments', () => {
134 | const selections = [
135 | {
136 | kind: 'Field',
137 | name: {
138 | kind: 'Name',
139 | value: 'allMessages',
140 | },
141 | arguments: [],
142 | selectionSet: {
143 | kind: 'SelectionSet',
144 | selections: [
145 | {
146 | kind: 'Field',
147 | name: {
148 | kind: 'Name',
149 | value: 'message',
150 | },
151 | arguments: [],
152 | },
153 | ],
154 | },
155 | },
156 | ];
157 |
158 | const result = magniClient.magniParser(selections);
159 | expect(result).toEqual(['{allMessages{message}}']);
160 | });
161 |
162 | it('should parse complex nested selections with arguments', () => {
163 | const selections = [
164 | {
165 | kind: 'Field',
166 | name: {
167 | kind: 'Name',
168 | value: 'messageById',
169 | },
170 | arguments: [
171 | {
172 | kind: 'Argument',
173 | name: {
174 | kind: 'Name',
175 | value: 'id',
176 | },
177 | value: {
178 | kind: 'StringValue',
179 | value: '4',
180 | },
181 | },
182 | ],
183 | selectionSet: {
184 | kind: 'SelectionSet',
185 | selections: [
186 | {
187 | kind: 'Field',
188 | name: {
189 | kind: 'Name',
190 | value: 'message',
191 | },
192 | arguments: [],
193 | },
194 | ],
195 | },
196 | },
197 | ];
198 |
199 | const result = magniClient.magniParser(selections);
200 | expect(result).toEqual(['{messageById(id:4){message}}']);
201 | });
202 |
203 | it('should handle multiple selections', () => {
204 | const selections = [
205 | {
206 | kind: 'Field',
207 | name: {
208 | kind: 'Name',
209 | value: 'allMessages',
210 | },
211 | arguments: [],
212 | },
213 | {
214 | kind: 'Field',
215 | name: {
216 | kind: 'Name',
217 | value: 'allUsers',
218 | },
219 | arguments: [],
220 | },
221 | ];
222 |
223 | const result = magniClient.magniParser(selections);
224 | expect(result).toEqual(['{allMessages}', '{allUsers}']);
225 | });
226 | });
227 |
--------------------------------------------------------------------------------
/__tests__/magnicache-demo.test.ts:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | // const express = require('express');
3 | const app = require('../magnicache-demo/server.js');
4 | const { IntrospectionQuery } = require('./IntrospectionQuery.js');
5 |
6 | describe('root Endpoint', () => {
7 | it('should respond with a html file', async () => {
8 | const response = await request(app)
9 | .get('/')
10 | .send()
11 | .set('Accept', 'application/html')
12 | .expect('Content-Type', /html/)
13 | .expect(200);
14 | });
15 | });
16 |
17 | describe('Graphql Endpoint', () => {
18 | it('should be a valid graphql endpoint', async () => {
19 | const response = await request(app)
20 | .post('/graphql')
21 | .send({ query: IntrospectionQuery })
22 | .set('Accept', 'application/json')
23 | .expect('Content-Type', /json/)
24 | .expect(200);
25 |
26 | expect(response.body.data).toBeDefined();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/__tests__/magnicache-server.test.ts:
--------------------------------------------------------------------------------
1 | const MagniCache = require('../magnicache-server/magnicache-server.ts');
2 | const schema = require('../magnicache-server/schema.js');
3 | import { NextFunction, Request, Response } from 'express';
4 | const { IntrospectionQuery } = require('./IntrospectionQuery');
5 | const { graphql } = require('graphql');
6 |
7 | //TODO: Set up proper error handling for invalid params
8 | describe('MagniCache Setup', () => {
9 | let magnicache: any;
10 | beforeEach((done) => {
11 | magnicache = new MagniCache(schema);
12 | done();
13 | });
14 |
15 | it('Should return a new magnicache object of type magnicache-server when invoked with a valid schema', () => {
16 | expect(magnicache).toBeInstanceOf(MagniCache);
17 | });
18 |
19 | it('Should have a query function on its prototype', () => {
20 | expect(typeof magnicache.query).toBe('function');
21 | });
22 |
23 | it('Should have a schemaParser function on its prototype', () => {
24 | expect(typeof magnicache.schemaParser).toBe('function');
25 | });
26 | });
27 |
28 | describe('Magnicache.query execution', () => {
29 | let magnicache: any;
30 | let mockReq: Partial;
31 | let mockRes: Partial;
32 | let nextFn: NextFunction = jest.fn();
33 |
34 | beforeEach((done) => {
35 | magnicache = new MagniCache(schema);
36 | mockReq = {};
37 | mockRes = {
38 | json: jest.fn(),
39 | cookie: jest.fn(),
40 | locals: {},
41 | };
42 | done();
43 | });
44 |
45 | //TODO: make the error handling for this better
46 | it('responds when missing query property', async () => {
47 | const expectedRes = {
48 | queryResponse: 'Invalid query',
49 | };
50 |
51 | mockReq = {
52 | body: {},
53 | };
54 |
55 | await magnicache.query(mockReq as Request, mockRes as Response, nextFn);
56 | expect(mockRes.locals!.queryResponse).toBe(expectedRes.queryResponse);
57 | });
58 |
59 | it('responds with a message when query value is empty', () => {
60 | const expectedRes = {
61 | queryResponse: 'Invalid query',
62 | };
63 |
64 | mockReq = {
65 | body: { query: '' },
66 | };
67 |
68 | magnicache.query(mockReq as Request, mockRes as Response, nextFn);
69 | expect(mockRes.locals!.queryResponse).toBe(expectedRes.queryResponse);
70 | });
71 |
72 | it('responds with a schema when query value is the Introspection query', async () => {
73 | const expectedRes = {
74 | queryResponse: { data: { __schema: {} } },
75 | };
76 |
77 | mockReq = {
78 | body: { query: IntrospectionQuery },
79 | };
80 |
81 | await magnicache.query(mockReq as Request, mockRes as Response, nextFn);
82 | expect(mockRes.locals!.queryResponse.data).toHaveProperty('__schema');
83 | });
84 |
85 | it('returns the right data when the query is one field ', async () => {
86 | // let data = await graphql(schema, 'query{customers{name}}');
87 |
88 | const expectedRes = {
89 | locals: { queryResponse: 'data' },
90 | };
91 |
92 | const mockReq: Partial = {
93 | body: { query: 'query{customers{name}}' },
94 | };
95 |
96 | const mockRes: any = {
97 | json: jest.fn(),
98 | cookie: jest.fn(),
99 | locals: { queryResponse: '' },
100 | };
101 |
102 | await magnicache.query(mockReq as Request, mockRes as Response, nextFn);
103 | expect(mockRes.locals.queryResponse).toEqual(
104 | expectedRes.locals.queryResponse
105 | );
106 | });
107 | });
108 |
109 | describe('MagniParser', () => {
110 | let magnicache: any;
111 | beforeEach(() => {
112 | magnicache = new MagniCache(schema);
113 | });
114 |
115 | it('should parse a simple query without arguments', () => {
116 | const selections = [
117 | {
118 | kind: 'Field',
119 | name: {
120 | kind: 'Name',
121 | value: 'allMessages',
122 | },
123 | arguments: [],
124 | },
125 | ];
126 |
127 | const result = magnicache.magniParser(selections);
128 | expect(result).toEqual(['{allMessages}']);
129 | });
130 |
131 | it('should parse a simple query with arguments', () => {
132 | const selections = [
133 | {
134 | kind: 'Field',
135 | name: {
136 | kind: 'Name',
137 | value: 'messageById',
138 | },
139 | arguments: [
140 | {
141 | kind: 'Argument',
142 | name: {
143 | kind: 'Name',
144 | value: 'id',
145 | },
146 | value: {
147 | kind: 'IntValue',
148 | value: '4',
149 | },
150 | },
151 | ],
152 | },
153 | ];
154 |
155 | const result = magnicache.magniParser(selections);
156 | expect(result).toEqual(['{messageById(id:4)}']);
157 | });
158 | });
159 |
--------------------------------------------------------------------------------
/assets/OGLOGO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/assets/OGLOGO.png
--------------------------------------------------------------------------------
/assets/Readme-graph.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/assets/Readme-graph.jpg
--------------------------------------------------------------------------------
/assets/RoundLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/assets/RoundLogo.png
--------------------------------------------------------------------------------
/assets/readmegif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/assets/readmegif.gif
--------------------------------------------------------------------------------
/client/.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-dom';
2 | declare module 'react-dom/client';
3 | declare module 'bootstrap';
4 | declare module 'magnicache-client.js';
5 |
--------------------------------------------------------------------------------
/client/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState, useEffect, useContext } from 'react';
3 | import NavBar from './components/NavBar';
4 | import MetricsContainer from './containers/MetricsContainer';
5 | import AboutPage from './containers/AboutPage';
6 | import DocsPage from './containers/DocsPage';
7 | import TeamPage from './containers/TeamPage';
8 | import './scss/styles.scss';
9 |
10 | import 'bootstrap/dist/css/bootstrap.min.css';
11 | import { Routes, Route, useNavigate, Link } from 'react-router-dom';
12 |
13 | // Create a type to easily add types to variables
14 | type Rtype = React.FC;
15 |
16 | const App: Rtype = () => {
17 | return (
18 |
19 |
20 |
21 | } />
22 | } />
23 | } />
24 | } />
25 |
26 |
27 | );
28 | };
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/client/assets/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/client/assets/Logo.png
--------------------------------------------------------------------------------
/client/assets/ahm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/client/assets/ahm.png
--------------------------------------------------------------------------------
/client/assets/aria.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/client/assets/aria.png
--------------------------------------------------------------------------------
/client/assets/tru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/client/assets/tru.png
--------------------------------------------------------------------------------
/client/assets/you.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/MagniCache/df197f09249d5689b77f3c69d0b21a8c3c0e3643/client/assets/you.png
--------------------------------------------------------------------------------
/client/components/CacheMetrics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Pie, Doughnut } from 'react-chartjs-2';
3 | import { CacheMetricsType } from '../../types';
4 |
5 | // Define interface for the props that will be passed to the component
6 | interface CacheProps {
7 | // Specify the type of the metrics prop to be used in this component
8 | metrics: CacheMetricsType;
9 | }
10 |
11 | // Create a new component named CacheMetrics which takes the CacheProps object as an input and returns JSX (template)
12 | const CacheMetrics = (props: CacheProps) => {
13 | // Destructure the metrics object from CacheProps object to avoid unnecessary repetition
14 | const { metrics } = props;
15 |
16 | // Deconstruct different properties from the metrics object to use later on
17 | const {
18 | cacheUsage,
19 | sizeLeft,
20 | totalHits,
21 | totalMisses,
22 | AvgCacheTime,
23 | AvgMissTime,
24 | AvgMemAccTime,
25 | } = metrics;
26 |
27 | // Define usageData object used to create data representation using chart.js library
28 | const usageData = {
29 | labels: ['Space Used', 'Space Left'],
30 | datasets: [
31 | {
32 | labels: ['Space Used', 'Space Left'],
33 | data: [cacheUsage, sizeLeft - cacheUsage],
34 | backgroundColor: ['#5b2af0', '#00CC99'],
35 | borderColor: ['white'],
36 | borderWidth: 1,
37 | maintainAspectRatio: true,
38 | responsive: true,
39 | },
40 | ],
41 | };
42 |
43 | // Define avgData object used to create data representation using chart.js library
44 | const avgData = {
45 | labels: ['Avg. Cached in ms', 'Avg. Uncached in ms'],
46 | datasets: [
47 | {
48 | data: [AvgCacheTime, AvgMissTime],
49 | backgroundColor: ['#5b2af0', '#b3001b'],
50 | borderColor: ['white'],
51 | borderWidth: 1,
52 | maintainAspectRatio: true,
53 | responsive: true,
54 | },
55 | ],
56 | };
57 |
58 | // Return JSX template to display metrics data in the HTML structure with dynamically updated data based on the data passed
59 | return (
60 |
61 |
Cache Capacity Used: {cacheUsage}
62 |
63 |
64 |
65 | Remaining Capacity: {sizeLeft - cacheUsage}
66 |
67 |
68 |
69 |
Total Hits: {totalHits}
70 |
71 |
72 |
Total Misses: {totalMisses}
73 |
74 |
75 |
76 | Average Miss Response Time: {Math.round(AvgMissTime)}ms
77 |
78 |
79 |
80 |
81 | Average Memory Access Time: {AvgMemAccTime}ms
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default CacheMetrics;
92 |
--------------------------------------------------------------------------------
/client/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Container from 'react-bootstrap/Container';
3 | import Nav from 'react-bootstrap/Nav';
4 | import Navbar from 'react-bootstrap/Navbar';
5 | import { Link } from 'react-router-dom';
6 | import logo from '../assets/Logo.png';
7 |
8 | // NavBar created with Bootstrap Navbar, Container, .Brand, .Link
9 | const NavBar: React.FC = () => {
10 | return (
11 |
12 |
13 |
14 | MagniCache
15 |
16 |
17 |
18 |
19 | Demo
20 |
21 |
22 | Installation
23 |
24 |
25 | Team
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default NavBar;
34 |
--------------------------------------------------------------------------------
/client/components/QueryDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import ToggleButton from 'react-bootstrap/ToggleButton';
3 | import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup';
4 | import Result from './Result';
5 | import XMetrics from '../containers/XMetrics';
6 | import { Metrics } from '../../types';
7 |
8 | // Create an interface for props to be destructered
9 | interface QueryProps {
10 | metrics: Metrics[];
11 | queryValue: string;
12 | setQueryValue: React.Dispatch>;
13 | queryResponse: Object;
14 | fetchTime: number;
15 | handleClickRun: () => void;
16 | handleClickClear: () => void;
17 | handleClearCache: () => void;
18 | key: string;
19 | clientMode: boolean;
20 | setClientMode: React.Dispatch>;
21 | handleSwitchMode: () => void;
22 | }
23 |
24 | // Destructure all props from the props object
25 | const QueryDisplay = (props: QueryProps) => {
26 | const {
27 | queryValue,
28 | setQueryValue,
29 | queryResponse,
30 | handleClickClear,
31 | handleClickRun,
32 | handleClearCache,
33 | handleSwitchMode,
34 | clientMode,
35 | } = props;
36 |
37 | const textAreaRef = useRef(null);
38 |
39 | const handleKeyDown: React.KeyboardEventHandler = (
40 | e: React.KeyboardEvent
41 | ): void => {
42 | if (e.key === 'Tab') {
43 | e.preventDefault();
44 |
45 | const textArea = textAreaRef.current;
46 | if (!textArea) return;
47 |
48 | const { selectionStart, selectionEnd } = textArea;
49 |
50 | const newValue =
51 | textArea.value.substring(0, selectionStart) +
52 | ' ' +
53 | textArea.value.substring(selectionEnd);
54 |
55 | textArea.value = newValue;
56 | textArea.selectionStart = textArea.selectionEnd = selectionStart + 1;
57 | setQueryValue(newValue);
58 | } else if (e.key === '{') {
59 | const textArea = textAreaRef.current;
60 | if (!textArea) return;
61 | const { selectionStart, selectionEnd } = textArea;
62 |
63 | const newValue =
64 | textArea.value.substring(0, selectionStart) +
65 | '}' +
66 | textArea.value.substring(selectionEnd);
67 | textArea.value = newValue;
68 | textArea.selectionStart = textArea.selectionEnd = selectionStart;
69 | setQueryValue(newValue);
70 | } else if (e.key === '(') {
71 | const textArea = textAreaRef.current;
72 | if (!textArea) return;
73 | const { selectionStart, selectionEnd } = textArea;
74 |
75 | const newValue =
76 | textArea.value.substring(0, selectionStart) +
77 | ')' +
78 | textArea.value.substring(selectionEnd);
79 | textArea.value = newValue;
80 | textArea.selectionStart = textArea.selectionEnd = selectionStart;
81 | setQueryValue(newValue);
82 | } else if (e.key === 'Enter') {
83 | const textArea = textAreaRef.current;
84 | if (!textArea) return;
85 | const { selectionStart, selectionEnd } = textArea;
86 | if (textArea.value[selectionStart - 1] === '{') {
87 | const newValue =
88 | textArea.value.substring(0, selectionStart) +
89 | '\n' +
90 | textArea.value.substring(selectionEnd);
91 | textArea.value = newValue;
92 | textArea.selectionStart = textArea.selectionEnd = selectionStart;
93 | setQueryValue(newValue);
94 | }
95 | }
96 | };
97 |
98 | return (
99 |
100 |
101 |
Query
102 |
103 |
111 |
117 |
129 | Run
130 |
131 |
132 |
143 | Clear
144 |
145 |
146 |
147 |
148 |
149 |
Results
150 |
151 |
152 |
153 |
159 |
171 | Switch to{' '}
172 | {clientMode ? 'Server-side caching' : 'Client-side caching'}
173 |
174 |
185 | Clear Cache
186 |
187 |
188 |
189 |
190 | );
191 | };
192 |
193 | export default QueryDisplay;
194 |
--------------------------------------------------------------------------------
/client/components/Response.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Metrics } from '../../types';
3 |
4 | // Declare an interface for props to be destructured
5 | interface ResponseProps {
6 | hits: number;
7 | misses: number;
8 | metrics: Metrics[];
9 | }
10 |
11 | const Response = (props: ResponseProps) => {
12 | // Destructure hits misses and metrics from the props object
13 | const { hits, misses, metrics } = props;
14 | return (
15 |
16 |
17 | Hits: {hits}
18 |
19 |
20 | Misses: {misses}
21 |
22 |
23 | Response Time: {''}
24 | {!metrics[0] ? '' : metrics[metrics.length - 1].fetchTime + 'ms'}
25 |
26 |
27 | );
28 | };
29 |
30 | export default Response;
31 |
--------------------------------------------------------------------------------
/client/components/Result.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Create interface for single prop to be passed down, queryResponse
4 | interface ResultProps {
5 | queryResponse: object;
6 | }
7 |
8 | const Result: React.FC = ({ queryResponse }) => {
9 |
10 | return (
11 | <>
12 | {/* Return a 'single' result on to the page. */}
13 |
14 | {JSON.stringify(
15 | queryResponse,
16 | null,
17 | 1.5
18 | )}
19 |
20 | >
21 | );
22 | };
23 |
24 | export default Result;
25 |
--------------------------------------------------------------------------------
/client/components/VisualsDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext, SetStateAction } from 'react';
2 | import {
3 | Chart as ChartJS,
4 | ArcElement,
5 | CategoryScale,
6 | LinearScale,
7 | PointElement,
8 | LineElement,
9 | Title,
10 | Tooltip,
11 | Legend,
12 | } from 'chart.js';
13 | import { Line, Pie } from 'react-chartjs-2';
14 | import Response from './Response';
15 | import { Metrics } from '../../types';
16 |
17 |
18 | // Register necessary elements from Chartjs
19 | ChartJS.register(
20 | ArcElement,
21 | CategoryScale,
22 | LinearScale,
23 | PointElement,
24 | LineElement,
25 |
26 | Title,
27 | Tooltip,
28 | Legend
29 | );
30 |
31 | // Declare interface for props to be passed down to VisualsDisplay
32 | interface VisualProps {
33 | queryValue: string;
34 | queryResponse: Object;
35 | metrics: Metrics[];
36 | setMetrics: React.Dispatch>;
37 | }
38 |
39 | // Create the charts within this file, response.tsx will take care of the metrics for the cache response
40 | const VisualsDisplay = (props: VisualProps) => {
41 |
42 | // Destructure metrics off of props
43 | // const { queryResponse, metrics } = props;
44 | const { metrics } = props;
45 |
46 | // Functions to get the amounts of hits and misses
47 | const hits = metrics.reduce((acc: number, curr: Metrics): number => {
48 | if (curr.cacheStatus === 'hit') {
49 | acc++;
50 | }
51 | return acc;
52 | }, 0);
53 | const misses = metrics.reduce((acc: number, curr: Metrics): number => {
54 | if (curr.cacheStatus === 'miss') {
55 | acc++;
56 | }
57 | return acc;
58 | }, 0);
59 |
60 | // Data for graph/pie chart
61 | const dataDo = {
62 | labels: ['Hits', 'Misses'],
63 | datasets: [
64 | {
65 | label: 'Cache Hits/Misses',
66 | data: [hits, misses],
67 | backgroundColor: ['#5b2af0', '#b3001b'],
68 | borderColor: ['white'],
69 | borderWidth: 1,
70 | },
71 | ],
72 | };
73 | // if first response is 0, replace it/slice the array
74 | const dataLine = {
75 | labels: metrics.map((obj) => {
76 | if (obj.cacheStatus === 'hit') {
77 | return 'Cached';
78 | } else if (obj.cacheStatus === 'miss') {
79 | return 'Uncached';
80 | }
81 | }),
82 | datasets: [
83 | {
84 | label: 'Response Time',
85 | data: metrics.map((obj) => obj.fetchTime),
86 | borderColor: '#5b2af0',
87 | backgroundColor: '#5b2af0',
88 | tension: 0.3,
89 | },
90 | ],
91 | };
92 | return (
93 | <>
94 |
95 |
96 |
97 |
105 |
106 |
107 |
108 |
109 |
110 |
113 |
114 |
115 | >
116 | );
117 | };
118 |
119 | export default VisualsDisplay;
120 |
--------------------------------------------------------------------------------
/client/containers/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | // Component to render about pages and info for MagniCache
5 | const AboutPage: React.FC = () => {
6 | return (
7 |
8 |
9 |
10 | MagniCache is a lightweight and performant GraphQL caching solution.
11 | Packaged and shipped as two separate npm packages, MagniCache can be
12 | implemented seamlessly into projects as an Express middleware for
13 | server-side caching of queries and mutations, or as a localStorage
14 | manipulation device for client-side caching.
15 |
16 |
17 | Try it out{' '}
18 |
19 | here
20 |
21 | .
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default AboutPage;
29 |
--------------------------------------------------------------------------------
/client/containers/DocsPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Component for rendering documentation page(TODO)
4 | const DocsPage: React.FC = () => {
5 | return (
6 |
7 |
Installation
8 |
9 |
10 |
Server-Side Caching
11 |
12 | 1. Install MagniCache Server.
13 | npm i @magnicache/server
14 |
15 |
16 | 2. Import MagniCache.
17 |
18 |
19 | const MagniCache = require('@magnicache/server');
20 |
21 |
22 |
23 | 3. Declare a new instance of MagniCache, passing in your GraphQL
24 | schema
25 |
26 |
27 | const magnicache = new MagniCache(schema);
28 |
29 |
30 |
31 | 4. Insert magnicache.query into the middleware chain for your
32 | '/graphql' route, while ensuring that all request bodies are parsed.
33 |
34 |
35 | app.use(express.json());
36 | app.use('/graphql', magnicache.query, (req, res) ={'>'} {'{'}{' '}
37 |
38 | {'return'} {'res'}.status(200).send(res.locals.queryResponse)
39 | {'}'}
40 | );
41 |
42 |
43 |
44 |
45 |
Client-Side Caching
46 |
47 | 1. Install MagniCache Client.
48 | npm i @magnicache/client
49 |
50 |
51 | 2. Import MagniCache.
52 |
53 |
54 | const MagniClient = require('@magnicache/client');
55 |
56 |
57 |
58 | 3. Declare a new instance of MagniClient, optionally passing in a
59 | cache capacity.
60 |
61 |
62 | const magniclient = new MagniClient(maxSize);
63 |
64 |
65 |
66 | 4. Invoke magniclient.query, passing in the query string and the
67 | graphql endpoint.
68 |
69 |
70 | magniclient.query(queryString, '/graphql');
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default DocsPage;
80 |
--------------------------------------------------------------------------------
/client/containers/MetricsContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import QueryDisplay from '../components/QueryDisplay';
3 | import VisualsDisplay from '../components/VisualsDisplay';
4 | import { Metrics } from '../../types';
5 |
6 | // Import and create a new instance of the magnicache client function
7 | const MagniClient = require('../magnicache-client.js');
8 | const magniClient = new MagniClient();
9 |
10 | const MetricsContainer: React.FC = () => {
11 | // Create state variables for the query, the response, the metrics, and if it is on client or server mode
12 | const [queryValue, setQueryValue] = useState(`query{
13 | allMessages{
14 | message
15 | message_id
16 | sender_id
17 | }
18 | }`);
19 | const [queryResponse, setQueryResponse] = useState({});
20 | const [metrics, setMetrics] = useState([]);
21 | const [clientMode, setClientMode] = useState(false);
22 | const [isThrottled, setThrottle] = useState(false);
23 | // Globally scope fetchtime variable
24 | let fetchTime = 0;
25 |
26 | // inside handleclickrun, proceed with functionality depending on whether server mode or client mode is activated
27 | const handleClickRun = () => {
28 | if (queryValue !== '' && queryValue !== null) {
29 | if (clientMode) {
30 | console.log('first response', queryResponse);
31 |
32 | const startTime = performance.now();
33 | magniClient
34 | .query(queryValue, '/graphql')
35 | // TODO: to clean up, try destructuring array in .then parameters
36 | .then((res: any): any => {
37 | //set all the metrics in this 'then' block
38 | let cacheStatus!: 'hit' | 'miss';
39 |
40 | const endTime = performance.now();
41 |
42 | let fetchTime = Math.round((endTime - startTime) * 100) / 100;
43 |
44 | res[1].uncached === true
45 | ? (cacheStatus = 'miss')
46 | : (cacheStatus = 'hit');
47 |
48 | setMetrics([...metrics, { cacheStatus, fetchTime }]);
49 |
50 | return res[0];
51 | })
52 | .then((data: {}) => {
53 | setQueryResponse(data);
54 | })
55 | .catch((err: {}) => console.log(err));
56 | console.log('second response', queryResponse);
57 | } else {
58 | const startTime = performance.now();
59 | // this fetch should only be invoked if server mode is activated
60 | fetch(`/graphql`, {
61 | method: 'POST',
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | },
65 | body: JSON.stringify({
66 | query: queryValue,
67 | }),
68 | })
69 | .then((res) => {
70 | //sett all the metrics in this 'then' block
71 | let cacheStatus!: 'hit' | 'miss';
72 | if (
73 | document.cookie
74 | .split(';')
75 | .some((cookie: string): boolean =>
76 | cookie.includes('cacheStatus=hit')
77 | )
78 | ) {
79 | cacheStatus = 'hit';
80 | }
81 | if (
82 | document.cookie
83 | .split(';')
84 | .some((cookie: string): boolean =>
85 | cookie.includes('cacheStatus=miss')
86 | )
87 | ) {
88 | cacheStatus = 'miss';
89 | }
90 | const endTime = performance.now();
91 | let fetchTime = Math.floor(endTime - startTime - 1);
92 | setMetrics([...metrics, { cacheStatus, fetchTime }]);
93 | return res.json();
94 | })
95 | .then((data: {}) => {
96 | setQueryResponse(data);
97 | })
98 | .catch((err) => console.log(err));
99 | }
100 | } else {
101 | setQueryResponse('Query field cannot be empty');
102 | }
103 | };
104 |
105 | const handleRunThrottle = () => {
106 | // If the function is still in a throttle return
107 | if (isThrottled) {
108 | return;
109 | }
110 | // Reassign throttle to be true if 'if' statement fails
111 | setThrottle(true);
112 |
113 | // AFter one second se tthe throttle back to false
114 | setTimeout(() => {
115 | setThrottle(false);
116 | }, 1000);
117 |
118 | // Invoke handle click run
119 | handleClickRun();
120 | };
121 |
122 | // Function to handle switching between client and server side caching
123 | const handleSwitchMode = () => {
124 | console.log('mode switched');
125 | setClientMode(!clientMode);
126 | setMetrics([]);
127 | setQueryResponse({});
128 | };
129 |
130 | // Function to clear query response from display
131 | const handleClickClear = () => {
132 | setQueryValue('');
133 | setQueryResponse({});
134 | };
135 |
136 | // Function for clearing cache at the click of a clear cache button
137 | const handleClearCache = () => {
138 | fetch(`/graphql`, {
139 | method: 'POST',
140 | headers: {
141 | 'Content-Type': 'application/json',
142 | },
143 | body: JSON.stringify({
144 | query: '{clearCache}',
145 | }),
146 | });
147 | };
148 |
149 | return (
150 |
151 |
152 |
166 |
167 |
168 |
175 |
176 |
177 | );
178 | };
179 |
180 | export default MetricsContainer;
181 |
--------------------------------------------------------------------------------
/client/containers/TeamPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ahmed from '../assets/ahm.png';
3 | import aria from '../assets/aria.png';
4 | import truman from '../assets/tru.png';
5 | import yousuf from '../assets/you.png';
6 |
7 | // Component for adding and rendering team page
8 | const TeamPage: React.FC = () => {
9 | return (
10 |
11 |
Our Team
12 |
The team that contributed to MagniCache.
13 |
14 |
38 |
62 |
86 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default TeamPage;
116 |
--------------------------------------------------------------------------------
/client/containers/XMetrics.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import CacheMetrics from '../components/CacheMetrics';
3 | import { CacheMetricsType } from '../../types';
4 |
5 | // Component for creating and rendering container for extra metrics(CacheMetrics)
6 | const XMetrics = () => {
7 | // Declare and type(Very specific) properties onto our metrics object
8 | const [metrics, setMetrics] = useState({
9 | cacheUsage: 0,
10 | sizeLeft: 0,
11 | totalHits: 0,
12 | totalMisses: 0,
13 | AvgCacheTime: 0,
14 | AvgMissTime: 0,
15 | AvgMemAccTime: 0,
16 | });
17 |
18 | // long polling function to send a request to retrieve the metrics from the server
19 | setTimeout(() => {
20 | fetch(`/graphql`, {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | },
25 | body: JSON.stringify({
26 | query: '{getMetrics}',
27 | }),
28 | })
29 | .then((res) => res.json())
30 | .then((data) => {
31 | setMetrics(data);
32 | });
33 | }, 10000);
34 | return (
35 | <>
36 |
37 |
Cache Metrics
38 |
39 |
40 | >
41 | );
42 | };
43 |
44 | export default XMetrics;
45 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | MagniCache
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 | hi
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/client/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | // import { createRoot } from 'react-dom/client';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import App from './App';
6 |
7 | // Create a type to easily add to variables
8 | type NodeType = HTMLElement | null;
9 |
10 | const domNode: NodeType = document.getElementById('root');
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | domNode
17 | );
18 |
19 |
--------------------------------------------------------------------------------
/client/magnicache-client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | exports.__esModule = true;
3 | var parse = require('graphql/language/parser').parse;
4 | var mergeWith = require('lodash.mergewith');
5 | function MagniClient(maxSize) {
6 | if (maxSize === void 0) {
7 | maxSize = 40;
8 | }
9 | this.maxSize = maxSize;
10 | // bind the method contexts
11 | this.query = this.query.bind(this);
12 | this.set = this.set.bind(this);
13 | this.get = this.get.bind(this);
14 | // instatiate our cache;
15 | var cache = localStorage.getItem('MagniClient');
16 | if (cache === null) {
17 | this.cache = [];
18 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
19 | } else {
20 | this.cache = JSON.parse(cache);
21 | }
22 | }
23 | MagniClient.prototype.set = function (query, value) {
24 | // add query to cache array (most recent is the back of the array)
25 | this.cache.push(query);
26 | // add value to localstorage
27 | localStorage.setItem(query, JSON.stringify(value));
28 | // check cache length, prune if nessesary
29 | while (this.cache.length > this.maxSize) {
30 | // remove least recent from front of array
31 | var itemToRemove = this.cache.shift();
32 | localStorage.removeItem(itemToRemove);
33 | }
34 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
35 | };
36 | MagniClient.prototype.get = function (query) {
37 | // get value from localstorage
38 | var value = localStorage.getItem(query);
39 | if (value === null) return {};
40 | // move the key to the end of the cache array
41 | var index = this.cache.indexOf(query);
42 | this.cache.splice(index, 1);
43 | this.cache.push(query);
44 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
45 | // return value
46 | return JSON.parse(value);
47 | };
48 | MagniClient.prototype.query = function (query, endpoint) {
49 | var _this = this;
50 | return new Promise(function (resolve, reject) {
51 | // parse query to obtain AST
52 | var ast = parse(query).definitions[0];
53 | // check that the operation is a query
54 | if (ast.operation === 'query') {
55 | // get the selection set
56 | var queries_2 = _this.magniParser(ast.selectionSet.selections);
57 | //store the query results
58 | var queryResponses_1 = [];
59 | // compile all individual query responses
60 | var compileQueries_1 = function () {
61 | var response = {};
62 | for (
63 | var _i = 0, queryResponses_2 = queryResponses_1;
64 | _i < queryResponses_2.length;
65 | _i++
66 | ) {
67 | var queryResponse = queryResponses_2[_i];
68 | response = mergeWith(response, queryResponse);
69 | }
70 | //whereas in server we saved response on res.locals to send back to client, here we just update the client side cache
71 | console.log(response);
72 | resolve([response, { uncached: uncached_1 }]);
73 | // resolve(response);
74 | };
75 | var uncached_1 = false;
76 | var _loop_1 = function (query_1) {
77 | // check if query is already cached
78 | if (_this.cache.includes(query_1)) {
79 | console.log('Client side cache hit');
80 | //store cached response
81 | queryResponses_1.push(_this.get(query_1));
82 | // check if all queries have been fetched
83 | if (queries_2.length === queryResponses_1.length) {
84 | compileQueries_1();
85 | }
86 | } else {
87 | // if query is not cached
88 | console.log('client side cache miss');
89 | uncached_1 = true;
90 | fetch(endpoint, {
91 | method: 'POST',
92 | body: JSON.stringify({ query: query_1 }),
93 | headers: {
94 | 'content-type': 'application/json',
95 | },
96 | })
97 | .then(function (data) {
98 | return data.json();
99 | })
100 | .then(function (result) {
101 | _this.set(query_1, result);
102 | queryResponses_1.push(result);
103 | if (queries_2.length === queryResponses_1.length) {
104 | console.log('this is the final compile queries');
105 | compileQueries_1();
106 | }
107 | })
108 | ['catch'](function (err) {
109 | console.log(err);
110 | reject(err);
111 | });
112 | }
113 | };
114 | for (var _i = 0, queries_1 = queries_2; _i < queries_1.length; _i++) {
115 | var query_1 = queries_1[_i];
116 | _loop_1(query_1);
117 | }
118 | }
119 | });
120 | };
121 | MagniClient.prototype.magniParser = function (selections, queryArray, queries) {
122 | var _a;
123 | if (queryArray === void 0) {
124 | queryArray = [];
125 | }
126 | if (queries === void 0) {
127 | queries = [];
128 | }
129 | //Logging that the parser is running
130 | console.log('parsing');
131 | // Looping through the selections to build the queries array
132 | for (var _i = 0, selections_1 = selections; _i < selections_1.length; _i++) {
133 | var selection = selections_1[_i];
134 | queryArray.push(selection.name.value);
135 | if (
136 | ((_a = selection.arguments) === null || _a === void 0
137 | ? void 0
138 | : _a.length) > 0
139 | ) {
140 | var argumentArray = [];
141 | // looping through the arguments to add them to the argument array
142 | for (var _b = 0, _c = selection.arguments; _b < _c.length; _b++) {
143 | var argument = _c[_b];
144 | argumentArray.push(
145 | ''.concat(argument.name.value, ':').concat(argument.value.value)
146 | );
147 | }
148 | //['id:4','name:john']
149 | queryArray.push([argumentArray.join(',')]);
150 | }
151 | // Checking for a selection set in the selection
152 | if (selection.selectionSet) {
153 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
154 | } else {
155 | var string = '';
156 | //{allMessages(id:4){message}}
157 | // Ex: ['messageById', ['id:4'], ['name:yousuf'], 'message']
158 | // would give {messageById(id:4,name:yousuf){message}}
159 | // Looping through the query array to build the string
160 | for (var i = queryArray.length - 1; i >= 0; i--) {
161 | if (Array.isArray(queryArray[i])) {
162 | string = '('.concat(queryArray[i][0], ')').concat(string);
163 | } else {
164 | string = '{'.concat(queryArray[i] + string, '}');
165 | }
166 | }
167 | // Adding the final string to the queries array
168 | queries.push(string);
169 | }
170 | // Removing the element from the query array
171 | queryArray.pop();
172 | }
173 | // Returning the queries array with all strings
174 | return queries;
175 | };
176 | module.exports = MagniClient;
177 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "magnicache-client",
3 | "version": "1.0.0",
4 | "description": "Client",
5 | "main": "index.tsx ",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "dependencies": {
10 | "bootstrap": "^5.2.3",
11 | "chart.js": "^4.2.1",
12 | "graphql": "^16.6.0",
13 | "lodash.mergewith": "^4.6.2",
14 | "react": "^17.0.2",
15 | "react-bootstrap": "^2.7.2",
16 | "react-chartjs-2": "^5.2.0",
17 | "react-dom": "^17.0.2",
18 | "react-router": "^4.3.1",
19 | "react-router-dom": "^6.8.0"
20 | },
21 | "devDependencies": {
22 | "@babel/core": "^7.20.12",
23 | "@babel/preset-env": "^7.20.2",
24 | "@babel/preset-react": "^7.18.6",
25 | "@monaco-editor/react": "^4.4.6",
26 | "@svgr/webpack": "^6.5.1",
27 | "@types/bootstrap": "^5.2.6",
28 | "@types/react": "^18.0.28",
29 | "@types/react-dom": "^18.0.11",
30 | "babel-loader": "^9.1.2",
31 | "concurrently": "^7.6.0",
32 | "css-loader": "^6.7.3",
33 | "file-loader": "^6.2.0",
34 | "html-webpack-plugin": "^5.5.0",
35 | "sass": "^1.57.1",
36 | "sass-loader": "^13.2.0",
37 | "style-loader": "^3.3.1",
38 | "webpack": "^5.75.0",
39 | "webpack-cli": "^5.0.1",
40 | "webpack-dev-server": "^4.11.1"
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/oslabs-beta/MagniCache.git"
45 | },
46 | "author": "MagniCache",
47 | "license": "MIT",
48 | "bugs": {
49 | "url": "https://github.com/oslabs-beta/MagniCache/issues"
50 | },
51 | "homepage": "https://github.com/oslabs-beta/MagniCache#readme"
52 | }
53 |
--------------------------------------------------------------------------------
/client/scss/styles.scss:
--------------------------------------------------------------------------------
1 | // @import '~bootstrap/scss/bootstrap';
2 | $borderRad: 14px;
3 | $SmallFont: #313533;
4 | $primaryColor: #5b2af0;
5 | $secondaryColor: #1a8fe3;
6 | $tertiaryColor: #d6fbff;
7 | $secondaryWhite: white;
8 | $blackColor: #313533;
9 | $errorColor: #b3001b;
10 | $lightTertiaryColor: rgba(214, 251, 255, 0.3);
11 | $good: #00cc99;
12 |
13 | body {
14 | background-image: linear-gradient(
15 | $lightTertiaryColor,
16 | $secondaryColor,
17 | $lightTertiaryColor
18 | );
19 | min-width: 990px;
20 | }
21 | // BODY AND COLOR DECLARATIONS ABOVE THIS LINE
22 | // BODY AND COLOR DECLARATIONS ABOVE THIS LINE
23 | // BODY AND COLOR DECLARATIONS ABOVE THIS LINE
24 | // BODY AND COLOR DECLARATIONS ABOVE THIS LINE
25 |
26 | .nav {
27 | border-bottom: 1px solid $blackColor;
28 | background-color: $primaryColor;
29 | color: $tertiaryColor;
30 | margin-bottom: 20px;
31 | .all-nav-links {
32 | display: flex;
33 | justify-content: right;
34 | #demo-nav-link,
35 | #install-nav-link,
36 | #team-nav-link {
37 | color: $secondaryWhite;
38 | margin-right: 4%;
39 | }
40 | }
41 | #nav-title {
42 | color: $secondaryWhite;
43 | font-size: 1.75em;
44 | }
45 | }
46 |
47 | .logo {
48 | width: 115px;
49 | height: 70px;
50 | margin-left: -20px;
51 | transition: transform 0.5s ease-in-out;
52 | }
53 |
54 | .logo:hover {
55 | animation: spin360 2.5s linear infinite;
56 | }
57 |
58 | @keyframes spin360 {
59 | from {
60 | transform: rotate(0deg);
61 | }
62 | to {
63 | transform: rotate(360deg);
64 | }
65 | }
66 |
67 | // ALL NAV BAR SELECTORS ABOVE THIS LINE
68 | // ALL NAV BAR SELECTORS ABOVE THIS LINE
69 | // ALL NAV BAR SELECTORS ABOVE THIS LINE
70 | // ALL NAV BAR SELECTORS ABOVE THIS LINE
71 |
72 | .maintainer {
73 | font-family: 'Roboto Mono', monospace;
74 | width: 100%;
75 | height: 100%;
76 | background-color: $lightTertiaryColor;
77 | }
78 |
79 | .metrics-container {
80 | display: flex;
81 | flex-direction: column;
82 | justify-content: start;
83 | align-items: center;
84 | background-color: $lightTertiaryColor;
85 | height: 100%;
86 | width: 100%;
87 | padding: 1.5%;
88 | }
89 |
90 | .metric-result-numbers {
91 | color: $blackColor;
92 | }
93 |
94 | // ALL MAINTAINER/MAIN METRICS CONTAINER SELECTORS ABOVE THIS LINE
95 | // ALL MAINTAINER/MAIN METRICS CONTAINER SELECTORS ABOVE THIS LINE
96 | // ALL MAINTAINER/MAIN METRICS CONTAINER SELECTORS ABOVE THIS LINE
97 | // ALL MAINTAINER/MAIN METRICS CONTAINER SELECTORS ABOVE THIS LINE
98 |
99 | .query-container,
100 | .visuals-container {
101 | width: 95%;
102 | border-radius: $borderRad;
103 | background-color: rgba(89, 48, 212, 0.412);
104 | box-shadow: 12px 12px 15px 1px rgba($color: white, $alpha: 0.4);
105 | .query-container {
106 | width: 85%;
107 | margin-bottom: 10px;
108 | }
109 | .query-display-flex {
110 | position: relative;
111 | padding-bottom: 10%;
112 | border-radius: $borderRad;
113 | display: grid;
114 | grid-template-columns: 1fr 1fr;
115 | padding-right: 6%;
116 | .query-display-child {
117 | padding-left: 1.5%;
118 | }
119 | }
120 | .query-display-title {
121 | text-align: center;
122 | padding: 0.5vw;
123 | margin-top: 1vw;
124 | font-size: 3vw;
125 | }
126 | }
127 |
128 | .fields-container {
129 | display: flex;
130 | flex-direction: column;
131 | justify-content: start;
132 | align-items: start;
133 | border-radius: $borderRad;
134 | width: 100%;
135 | margin-bottom: 8px;
136 | min-height: 150px;
137 | max-height: 300px;
138 | overflow-y: scroll;
139 | }
140 |
141 | .fields-container-result::-webkit-scrollbar {
142 | display: none;
143 | }
144 | .fields-container::-webkit-scrollbar {
145 | display: none;
146 | }
147 |
148 | .query-input {
149 | color: $blackColor;
150 | background-color: $lightTertiaryColor;
151 | border-radius: $borderRad;
152 | font-size: 1.3vw;
153 | height: 9rem;
154 | text-align: left;
155 | resize: none;
156 | border: none;
157 | margin: 0;
158 | padding: 0.5vw;
159 | position: absolute;
160 | top: 15%;
161 | width: 44vw;
162 | #query-box {
163 | display: flex;
164 | flex-direction: column;
165 | align-items: center;
166 | }
167 | }
168 |
169 | .query-input:focus {
170 | outline-width: 0;
171 | }
172 |
173 | .fields-container::-webkit-scrollbar,
174 | .fields-container-result::-webkit-scrollbar {
175 | display: none;
176 | }
177 | .query-input::-webkit-scrollbar {
178 | display: none;
179 | }
180 |
181 | .query-display-child .toggle-button-group-rc {
182 | width: 20%;
183 | }
184 |
185 | .fields-container-result {
186 | margin-top: -44%;
187 | align-items: start;
188 | padding-left: 8px;
189 | padding-top: 8px;
190 | margin-right: 10px;
191 | border-radius: $borderRad;
192 | min-height: 250px;
193 | max-height: 250px;
194 | overflow-y: scroll;
195 | overflow-x: hidden;
196 | background-color: $lightTertiaryColor;
197 | color: $blackColor;
198 | display: flex;
199 | flex-direction: column;
200 | justify-content: start;
201 | width: 48%;
202 | overflow-wrap: break-word;
203 | position: absolute;
204 | bottom: 10%;
205 | .single-result {
206 | word-break: break-all;
207 | font-size: 1rem;
208 | white-space: pre-wrap;
209 | }
210 | }
211 |
212 | .query-result-title {
213 | text-align: center;
214 | padding: 0.5vw;
215 | margin-top: 1vw;
216 | font-size: 3vw;
217 | position: absolute;
218 | bottom: 45%;
219 | left: 16%;
220 | }
221 |
222 | #toggle-button-cache {
223 | position: absolute;
224 | top: 92%;
225 | }
226 |
227 | .extra-metrics-container {
228 | margin-top: 14%;
229 | color: $blackColor;
230 | background-color: $lightTertiaryColor;
231 | border-radius: $borderRad;
232 | max-height: 110%;
233 | display: flex;
234 | flex-direction: column;
235 | align-items: start;
236 | justify-content: start;
237 | padding-left: 18px;
238 | padding-top: 20px;
239 | padding-bottom: 2%;
240 | margin-left: 20%;
241 | }
242 |
243 | .cache-metrics-title {
244 | position: absolute;
245 | top: -1.3%;
246 | right: 12%;
247 | text-align: center;
248 | padding: 0.5vw;
249 | margin-top: 2vw;
250 | font-size: 3vw;
251 | }
252 |
253 | .xtra-dough {
254 | position: absolute;
255 | right: 8%;
256 | top: 40%;
257 | }
258 |
259 | .half-metrics {
260 | position: absolute;
261 | right: 8%;
262 | top: 16.4%;
263 | }
264 |
265 | // ALL QUERY/RESULT SELECTORS ABOVE THIS LINE
266 | // ALL QUERY/RESULT SELECTORS ABOVE THIS LINE
267 | // ALL QUERY/RESULT SELECTORS ABOVE THIS LINE
268 | // ALL QUERY/RESULT SELECTORS ABOVE THIS LINE
269 |
270 | .visuals-container {
271 | min-height: 400px;
272 | margin-top: 1vh;
273 | #visuals-header {
274 | text-align: center;
275 | font-size: 3.5vw;
276 | margin-top: 1%;
277 | }
278 | .visuals-display {
279 | display: flex;
280 | justify-content: space-evenly;
281 | align-items: center;
282 | padding: 0;
283 | margin: 0;
284 | }
285 | }
286 |
287 | .left-visual,
288 | .hits-misses,
289 | .donut-chart {
290 | border-radius: $borderRad;
291 | padding-left: 8px;
292 | padding-top: 8px;
293 | margin: 1%;
294 | }
295 |
296 | .left-visual,
297 | .right-visual {
298 | height: 100%;
299 | margin: 0.5vw;
300 | }
301 |
302 | .left-visual {
303 | padding-bottom: 1vw;
304 | width: 60%;
305 | height: 300px;
306 | min-width: 540px;
307 | background-color: $lightTertiaryColor;
308 | }
309 |
310 | .right-visual {
311 | display: flex;
312 | padding: 0;
313 | .donut-chart {
314 | min-width: 250px;
315 | padding-bottom: 1vw;
316 | margin: 0;
317 | margin-left: 0.7vw;
318 | }
319 | }
320 |
321 | .hits-misses {
322 | padding: 1vw;
323 | width: 45%;
324 | margin: 0;
325 | color: $secondaryColor;
326 | font-weight: 600;
327 | line-height: 2;
328 | width: 250px;
329 | #hits {
330 | color: $primaryColor;
331 | }
332 | #misses {
333 | color: $errorColor;
334 | }
335 | #response-time {
336 | color: $blackColor;
337 | }
338 | }
339 |
340 | // ALL VISUALS/VISCONTAINER SELECTORS ABOVE THIS LINE
341 | // ALL VISUALS/VISCONTAINER SELECTORS ABOVE THIS LINE
342 | // ALL VISUALS/VISCONTAINER SELECTORS ABOVE THIS LINE
343 | // ALL VISUALS/VISCONTAINER SELECTORS ABOVE THIS LINE
344 |
345 | #team-page,
346 | #about-page,
347 | #docs-page {
348 | background-color: $lightTertiaryColor;
349 | width: 100vw;
350 | height: 100vh;
351 | text-align: center;
352 | }
353 |
354 | #team-page {
355 | display: flex;
356 | flex-direction: column;
357 | }
358 |
359 | #team-page h1,
360 | #about-page h1,
361 | #docs-page h1 {
362 | font-size: 3rem;
363 | padding-top: 3%;
364 | }
365 |
366 | .docs-flexbox {
367 | display: flex;
368 | justify-content: space-around;
369 | margin: 2%;
370 | }
371 |
372 | .docs-item {
373 | width: 45%;
374 | }
375 |
376 | .docs-title {
377 | margin-bottom: 5%;
378 | font-weight: 700;
379 | text-decoration: underline;
380 | }
381 |
382 | #team-description {
383 | margin: 4%;
384 | }
385 |
386 | #team-members {
387 | display: flex;
388 | justify-content: space-evenly;
389 | align-items: center;
390 | }
391 |
392 | .member-img {
393 | width: 70%;
394 | height: 70%;
395 | // width: 280px;
396 | // height: 280px;
397 | border-radius: 16px;
398 | }
399 |
400 | .member-name {
401 | margin-top: 2%;
402 | font-size: 1.6em;
403 | font-weight: 500;
404 | color: $primaryColor;
405 | }
406 |
407 | .member-links {
408 | font-size: 1em;
409 | font-weight: 400;
410 | color: $secondaryColor;
411 | text-decoration: none;
412 | }
413 |
414 | .member-links:hover {
415 | text-decoration: none;
416 | }
417 |
418 | #about-page {
419 | display: flex;
420 | flex-direction: column;
421 | justify-content: flex-start;
422 | align-items: center;
423 | }
424 |
425 | .about-code {
426 | background-color: #a5a5a570;
427 | border-radius: 6px;
428 | padding: 12px 6px 12px 6px;
429 | line-height: 3;
430 | font-weight: 100;
431 | }
432 |
433 | .about-magnicache {
434 | width: 75%;
435 | font-size: 1.2rem;
436 | font-weight: 400;
437 | margin-top: 10%;
438 | }
439 |
440 | // ALL PAGES AND THEIR SELECTORS ABOVE THIS LINE
441 | // ALL PAGES AND THEIR SELECTORS ABOVE THIS LINE
442 | // ALL PAGES AND THEIR SELECTORS ABOVE THIS LINE
443 | // ALL PAGES AND THEIR SELECTORS ABOVE THIS LINE
444 |
445 | // 1378 pixels maximum
446 | @media (max-width: 1771px) {
447 | #query-box {
448 | display: column;
449 | flex-direction: row;
450 | position: relative;
451 | }
452 | .query-input {
453 | width: 92%;
454 | top: 16%;
455 | }
456 | .query-result-title {
457 | position: absolute;
458 | bottom: 51.5%;
459 | left: 16%;
460 | }
461 | .fields-container-result {
462 | position: absolute;
463 | width: 45%;
464 | bottom: 16%;
465 | }
466 | #toggle-button-cache {
467 | display: flex;
468 | flex-direction: column;
469 | position: absolute;
470 | top: 86%;
471 | }
472 | .xtra-dough {
473 | right: 4%;
474 | top: 35.5%;
475 | scale: 1;
476 | }
477 | .half-metrics {
478 | top: 14.5%;
479 | right: 5%;
480 | }
481 | .extra-metrics-container {
482 | width: 95%;
483 | margin-left: 10%;
484 | }
485 | }
486 |
487 | @media (max-width: 1510px) {
488 | .xtra-dough {
489 | top: 69%;
490 | left: 57%;
491 | scale: 1.2;
492 | }
493 | .xtra-pie {
494 | position: absolute;
495 | bottom: 35%;
496 | left: 57%;
497 | scale: 1.2;
498 | }
499 | .extra-metrics-container {
500 | padding-bottom: 90%;
501 | width: 65%;
502 | height: 100%;
503 | margin-right: -100px;
504 | }
505 | .cache-metrics-title {
506 | left: 46%;
507 | }
508 | .half-metrics {
509 | top: 30%;
510 | left: 53.1%;
511 | }
512 | #toggle-button-cache {
513 | top: 64%;
514 | }
515 | .query-input {
516 | top: 10%;
517 | }
518 | .query-result-title {
519 | bottom: 63%;
520 | }
521 | .fields-container-result {
522 | bottom: 38%;
523 | }
524 | }
525 |
526 | @media (max-width: 1466px) {
527 | .extra-metrics-container {
528 | padding-bottom: 135%;
529 | width: 90%;
530 | }
531 | .query-input {
532 | top: 8%;
533 | }
534 | .cache-metrics-title {
535 | left: 57%;
536 | }
537 | }
538 |
539 | @media (max-width: 1235px) {
540 | .query-input {
541 | top: 8%;
542 | }
543 | .half-metrics {
544 | top: 28%;
545 | }
546 | }
547 |
548 | @media (max-width: 1048px) {
549 | .xtra-dough {
550 | top: 72%;
551 | }
552 | .xtra-pie {
553 | top: 46%;
554 | }
555 | }
556 |
557 | @media (max-width: 989px) {
558 | .xtra-dough {
559 | bottom: 0;
560 | }
561 | }
562 |
563 | @media (max-width: 1264px) {
564 | .query-result-title {
565 | top: 25%;
566 | }
567 | }
568 |
569 | @media (max-width: 1072px) {
570 | .query-result-title {
571 | top: 29%;
572 | }
573 | }
574 |
575 | @media (max-width: 1235px) {
576 | .visuals-display {
577 | display: flex;
578 | flex-direction: column;
579 | }
580 | .query-input,
581 | .single-result {
582 | font-size: 1.6vw;
583 | }
584 | }
585 |
586 | @media (max-width: 866px) {
587 | .query-input,
588 | .single-result {
589 | font-size: 1.9vw;
590 | }
591 | }
592 |
593 | @media (min-width: 1920px) {
594 | }
595 |
596 | // ALL MEDIA QUERIES ABOVE THIS LINE
597 | // ALL MEDIA QUERIES ABOVE THIS LINE
598 | // ALL MEDIA QUERIES ABOVE THIS LINE
599 | // ALL MEDIA QUERIES ABOVE THIS LINE
600 | // ALL MEDIA QUERIES ABOVE THIS LINE
601 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | module.exports = {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/private/var/folders/0r/9d1rbwl5693_030d56l3cj040000gn/T/jest_dx",
15 |
16 | // Automatically clear mock calls, instances, contexts and results before every test
17 | // clearMocks: false,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | // collectCoverage: false,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | // collectCoverageFrom: undefined,
24 |
25 | // The directory where Jest should output its coverage files
26 | // coverageDirectory: undefined,
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | coveragePathIgnorePatterns: ['/node_modules/'],
30 |
31 | // Indicates which provider should be used to instrument code for coverage
32 | coverageProvider: 'v8',
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: undefined,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: undefined,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // The default configuration for fake timers
52 | // fakeTimers: {
53 | // "enableGlobally": false
54 | // },
55 |
56 | // Force coverage collection from ignored files using an array of glob patterns
57 | // forceCoverageMatch: [],
58 |
59 | // A path to a module which exports an async function that is triggered once before all test suites
60 | // globalSetup: undefined,
61 |
62 | // A path to a module which exports an async function that is triggered once after all test suites
63 | // globalTeardown: undefined,
64 |
65 | // A set of global variables that need to be available in all test environments
66 | // globals: {},
67 |
68 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
69 | // maxWorkers: "50%",
70 |
71 | // An array of directory names to be searched recursively up from the requiring module's location
72 | moduleDirectories: ['node_modules'],
73 |
74 | // An array of file extensions your modules use
75 | // moduleFileExtensions: [
76 | // "js",
77 | // "mjs",
78 | // "cjs",
79 | // "jsx",
80 | // "ts",
81 | // "tsx",
82 | // "json",
83 | // "node"
84 | // ],
85 |
86 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
87 | // moduleNameMapper: {},
88 |
89 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
90 | // modulePathIgnorePatterns: [],
91 |
92 | // Activates notifications for test results
93 | // notify: false,
94 |
95 | // An enum that specifies notification mode. Requires { notify: true }
96 | // notifyMode: "failure-change",
97 |
98 | // A preset that is used as a base for Jest's configuration
99 | preset: 'ts-jest',
100 |
101 | // Run tests from one or more projects
102 | // projects: undefined,
103 |
104 | // Use this configuration option to add custom reporters to Jest
105 | // reporters: undefined,
106 |
107 | // Automatically reset mock state before every test
108 | // resetMocks: false,
109 |
110 | // Reset the module registry before running each individual test
111 | // resetModules: false,
112 |
113 | // A path to a custom resolver
114 | // resolver: undefined,
115 |
116 | // Automatically restore mock state and implementation before every test
117 | // restoreMocks: false,
118 |
119 | // The root directory that Jest should scan for tests and modules within
120 | // rootDir: undefined,
121 |
122 | // A list of paths to directories that Jest should use to search for files in
123 | // roots: [
124 | // ""
125 | // ],
126 |
127 | // Allows you to use a custom runner instead of Jest's default test runner
128 | // runner: "jest-runner",
129 |
130 | // The paths to modules that run some code to configure or set up the testing environment before each test
131 | // setupFiles: [],
132 |
133 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
134 | // setupFilesAfterEnv: [],
135 |
136 | // The number of seconds after which a test is considered as slow and reported as such in the results.
137 | // slowTestThreshold: 5,
138 |
139 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
140 | // snapshotSerializers: [],
141 |
142 | // The test environment that will be used for testing
143 | // testEnvironment: "jest-environment-node",
144 |
145 | // Options that will be passed to the testEnvironment
146 | // testEnvironmentOptions: {},
147 |
148 | // Adds a location field to test results
149 | // testLocationInResults: false,
150 |
151 | // The glob patterns Jest uses to detect test files
152 | testMatch: [
153 | '**/__tests__/**/*.[jt]s?(x)',
154 | // "**/?(*.)+(spec|test).[tj]s?(x)"
155 | ],
156 |
157 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
158 | testPathIgnorePatterns: ['/node_modules/'],
159 |
160 | // The regexp pattern or array of patterns that Jest uses to detect test files
161 | // testRegex: [],
162 |
163 | // This option allows the use of a custom results processor
164 | // testResultsProcessor: undefined,
165 |
166 | // This option allows use of a custom test runner
167 | // testRunner: "jest-circus/runner",
168 |
169 | // A map from regular expressions to paths to transformers
170 | // transform: undefined,
171 |
172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
173 | // transformIgnorePatterns: [
174 | // "/node_modules/",
175 | // "\\.pnp\\.[^\\/]+$"
176 | // ],
177 |
178 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
179 | // unmockedModulePathPatterns: undefined,
180 |
181 | // Indicates whether each individual test should be reported during the run
182 | // verbose: undefined,
183 |
184 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
185 | // watchPathIgnorePatterns: [],
186 |
187 | // Whether to use watchman for file crawling
188 | // watchman: true,
189 | };
190 |
--------------------------------------------------------------------------------
/magnicache-client/README.md:
--------------------------------------------------------------------------------
1 | # Magnicache/client
2 |
3 |
4 |
5 | A lightweight client-side GraphQL caching solution that optimizes queries by atomizing complex queries and caching them individually.
6 |
7 | MagniClient utilizes localStorage to cache query results across sessions.
8 |
9 | Configurable cache size.
10 |
11 |
12 |
13 | ## How to use @magnicache/client in your GraphQL client
14 |
15 | 1. Install MagniCache Client.
16 |
17 | ```bash
18 | npm i @magnicache/client
19 | ```
20 |
21 | 2. Import MagniCache Client.
22 |
23 | ```js
24 | const MagniClient = require('@magnicache/client');
25 | ```
26 |
27 | 3. Declare a new instance of MagniClient, optionally passing in a cache capacity(defaults to 40).
28 |
29 | ```js
30 | const magniClient = new MagniClient(maxSize);
31 | ```
32 |
33 | 4. Invoke magniClient.query, passing in the query string and the graphql endpoint.
34 |
35 | ```js
36 | magniClient
37 | .query(
38 | `query {
39 | person(personID: 1) {
40 | name
41 | eyeColor
42 | height
43 | }
44 | }`,
45 | '/graphql'
46 | )
47 | .then((response) => {
48 | const queryData = response[0];
49 | const cacheStatus = response[1];
50 | // Handle the response data
51 | })
52 | .catch((err) => {
53 | // Handle errors
54 | });
55 | ```
56 |
57 | ## Contributors
58 |
59 | [Ahmed Chami](https://www.linkedin.com/in/ahmed-chami/)
60 |
61 | [Aria Soltankhah](https://www.linkedin.com/in/ariasol/)
62 |
63 | [Truman Miller](https://www.linkedin.com/in/truman-miller)
64 |
65 | [Yousuf Elkhoga](https://www.linkedin.com/in/yousufelkhoga/)
66 |
--------------------------------------------------------------------------------
/magnicache-client/magnicache-client.js:
--------------------------------------------------------------------------------
1 | var parse = require('graphql/language/parser').parse;
2 | var mergeWith = require('lodash.mergewith');
3 | // import * as mergeWith from 'lodash.mergewith';
4 | function MagniClient(maxSize) {
5 | if (maxSize === void 0) { maxSize = 40; }
6 | this.maxSize = maxSize;
7 | // bind the method contexts
8 | this.query = this.query.bind(this);
9 | this.set = this.set.bind(this);
10 | this.get = this.get.bind(this);
11 | // instantiate our cache;
12 | var cache = localStorage.getItem('MagniClient');
13 | if (cache === null) {
14 | this.cache = [];
15 | // TODO: check if this line is even nesaccary
16 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
17 | }
18 | else {
19 | // make sure there are no duplicate in our cache array
20 | this.cache = Array.from(new Set(JSON.parse(cache)));
21 | }
22 | }
23 | MagniClient.prototype.set = function (query, value) {
24 | // add query to cache array (most recent is the back of the array)
25 | this.cache.push(query);
26 | // add value to localstorage
27 | localStorage.setItem(query, JSON.stringify(value));
28 | // check cache length, prune if necessary
29 | while (this.cache.length > this.maxSize) {
30 | // remove least recent from front of array
31 | var itemToRemove = this.cache.shift();
32 | localStorage.removeItem(itemToRemove);
33 | }
34 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
35 | };
36 | MagniClient.prototype.get = function (query) {
37 | // get value from localstorage
38 | var value = localStorage.getItem(query);
39 | // TODO: improve error handling
40 | if (value === null)
41 | return {};
42 | // move the key to the end of the cache array
43 | var index = this.cache.indexOf(query);
44 | this.cache.splice(index, 1);
45 | this.cache.push(query);
46 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
47 | // return value
48 | return JSON.parse(value);
49 | };
50 | MagniClient.prototype.query = function (query, endpoint) {
51 | var _this = this;
52 | return new Promise(function (resolve, reject) {
53 | // parse query to obtain AST
54 | var ast = parse(query).definitions[0];
55 | // check that the operation is a query
56 | if (ast.operation === 'query') {
57 | // get the selection set
58 | var queries_2 = _this.magniParser(ast.selectionSet.selections);
59 | //store the query results
60 | var queryResponses_1 = [];
61 | // compile all individual query responses
62 | var compileQueries_1 = function () {
63 | var response = {};
64 | for (var _i = 0, queryResponses_2 = queryResponses_1; _i < queryResponses_2.length; _i++) {
65 | var queryResponse = queryResponses_2[_i];
66 | response = mergeWith(response, queryResponse);
67 | }
68 | //whereas in server we saved response on res.locals to send back to client, here we just update the client side cache
69 | console.log(response);
70 | resolve([response, { uncached: uncached_1 }]);
71 | };
72 | var uncached_1 = false; // cache hit
73 | var _loop_1 = function (query_1) {
74 | // check if query is already cached
75 | if (_this.cache.includes(query_1)) {
76 | console.log('Client-side cache hit');
77 | //store cached response
78 | queryResponses_1.push(_this.get(query_1));
79 | // check if all queries have been fetched
80 | if (queries_2.length === queryResponses_1.length) {
81 | compileQueries_1();
82 | }
83 | }
84 | else {
85 | // if query is not cached
86 | console.log('Client-side cache miss');
87 | uncached_1 = true; // cache miss
88 | fetch(endpoint, {
89 | method: 'POST',
90 | body: JSON.stringify({ query: query_1 }),
91 | headers: {
92 | 'content-type': 'application/json'
93 | }
94 | })
95 | .then(function (data) { return data.json(); })
96 | .then(function (result) {
97 | if (!result.err)
98 | _this.set(query_1, result);
99 | queryResponses_1.push(result);
100 | if (queries_2.length === queryResponses_1.length) {
101 | compileQueries_1();
102 | }
103 | })["catch"](function (err) {
104 | console.log(err);
105 | reject(err);
106 | });
107 | }
108 | };
109 | for (var _i = 0, queries_1 = queries_2; _i < queries_1.length; _i++) {
110 | var query_1 = queries_1[_i];
111 | _loop_1(query_1);
112 | }
113 | }
114 | });
115 | };
116 | MagniClient.prototype.magniParser = function (selections, queryArray, queries) {
117 | var _a;
118 | if (queryArray === void 0) { queryArray = []; }
119 | if (queries === void 0) { queries = []; }
120 | // Looping through the selections to build the queries array
121 | for (var _i = 0, selections_1 = selections; _i < selections_1.length; _i++) {
122 | var selection = selections_1[_i];
123 | // add current query to queryArray
124 | queryArray.push(selection.name.value);
125 | // if a query has arguments, format and add to query array
126 | if (((_a = selection.arguments) === null || _a === void 0 ? void 0 : _a.length) > 0) {
127 | var argumentArray = [];
128 | // looping through the arguments to add them to the argument array
129 | for (var _b = 0, _c = selection.arguments; _b < _c.length; _b++) {
130 | var argument = _c[_b];
131 | argumentArray.push("".concat(argument.name.value, ":").concat(argument.value.value));
132 | }
133 | queryArray.push([argumentArray.join(',')]);
134 | }
135 | // if there is a selectionSet property, there are more deeply nested queries
136 | if (selection.selectionSet) {
137 | // recursively invoke magniParser passing in selections array
138 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
139 | }
140 | else {
141 | var string = "";
142 | // if query array looks like this: ['messageById', ['id:4'], 'message']
143 | // formated query will look like this: {allMessages(id:4){message}}
144 | // looping through the query array in reverse to build the query string
145 | for (var i = queryArray.length - 1; i >= 0; i--) {
146 | // arguments are put into an array and need to be formatted differently
147 | if (Array.isArray(queryArray[i])) {
148 | string = "(".concat(queryArray[i][0], ")").concat(string);
149 | }
150 | else {
151 | string = "{".concat(queryArray[i] + string, "}");
152 | }
153 | }
154 | // adding the completed query to the queries array
155 | queries.push(string);
156 | }
157 | // remove last element, as it's not wanted on next iteration
158 | queryArray.pop();
159 | }
160 | // returning the array of individual querys
161 | return queries;
162 | };
163 | // export default MagniClient;
164 | module.exports = MagniClient;
165 |
--------------------------------------------------------------------------------
/magnicache-client/magnicache-client.ts:
--------------------------------------------------------------------------------
1 | const { parse } = require('graphql/language/parser');
2 | const mergeWith = require('lodash.mergewith');
3 | // import * as mergeWith from 'lodash.mergewith';
4 |
5 | function MagniClient(this: any, maxSize: number = 40): void {
6 | this.maxSize = maxSize;
7 | // bind the method contexts
8 | this.query = this.query.bind(this);
9 | this.set = this.set.bind(this);
10 | this.get = this.get.bind(this);
11 |
12 | // instantiate our cache;
13 | const cache = localStorage.getItem('MagniClient');
14 | if (cache === null) {
15 | this.cache = [];
16 | // TODO: check if this line is even nesaccary
17 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
18 | } else {
19 | // make sure there are no duplicate in our cache array
20 | this.cache = Array.from(new Set(JSON.parse(cache)));
21 | }
22 | }
23 |
24 | MagniClient.prototype.set = function (query: string, value: {}): void {
25 | // add query to cache array (most recent is the back of the array)
26 | this.cache.push(query);
27 |
28 | // add value to localstorage
29 | localStorage.setItem(query, JSON.stringify(value));
30 | // check cache length, prune if necessary
31 | while (this.cache.length > this.maxSize) {
32 | // remove least recent from front of array
33 | const itemToRemove = this.cache.shift();
34 | localStorage.removeItem(itemToRemove);
35 | }
36 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
37 | };
38 |
39 | MagniClient.prototype.get = function (query: string): {} {
40 | // get value from localstorage
41 | const value = localStorage.getItem(query);
42 | // TODO: improve error handling
43 | if (value === null) return {};
44 | // move the key to the end of the cache array
45 | const index = this.cache.indexOf(query);
46 | this.cache.splice(index, 1);
47 | this.cache.push(query);
48 | localStorage.setItem('MagniClient', JSON.stringify(this.cache));
49 | // return value
50 | return JSON.parse(value);
51 | };
52 |
53 | MagniClient.prototype.query = function (query: string, endpoint: string) {
54 | return new Promise((resolve, reject) => {
55 | // parse query to obtain AST
56 | const {
57 | definitions: [ast],
58 | } = parse(query);
59 |
60 | // check that the operation is a query
61 | if (ast.operation === 'query') {
62 | // get the selection set
63 | const queries: string[] = this.magniParser(ast.selectionSet.selections);
64 |
65 | //store the query results
66 | const queryResponses: {}[] = [];
67 |
68 | // compile all individual query responses
69 | const compileQueries = () => {
70 | let response: {} = {};
71 |
72 | for (const queryResponse of queryResponses) {
73 | response = mergeWith(response, queryResponse);
74 | }
75 | //whereas in server we saved response on res.locals to send back to client, here we just update the client side cache
76 | console.log(response);
77 | resolve([response, { uncached }]);
78 | };
79 | let uncached = false; // cache hit
80 |
81 | for (const query of queries) {
82 | // check if query is already cached
83 | if (this.cache.includes(query)) {
84 | console.log('Client-side cache hit');
85 | //store cached response
86 |
87 | queryResponses.push(this.get(query));
88 |
89 | // check if all queries have been fetched
90 | if (queries.length === queryResponses.length) {
91 | compileQueries();
92 | }
93 | } else {
94 | // if query is not cached
95 |
96 | console.log('Client-side cache miss');
97 | uncached = true; // cache miss
98 |
99 | fetch(endpoint, {
100 | method: 'POST',
101 | body: JSON.stringify({ query }),
102 | headers: {
103 | 'content-type': 'application/json',
104 | },
105 | })
106 | .then((data) => data.json())
107 | .then((result: { err?: {}; data?: {} }) => {
108 | if (!result.err) this.set(query, result);
109 | queryResponses.push(result);
110 |
111 | if (queries.length === queryResponses.length) {
112 | compileQueries();
113 | }
114 | })
115 | .catch((err: {}) => {
116 | console.log(err);
117 | reject(err);
118 | });
119 | }
120 | }
121 | }
122 | });
123 | };
124 |
125 | MagniClient.prototype.magniParser = function (
126 | selections: {
127 | kind: string;
128 | name: {
129 | kind: string;
130 | value: string;
131 | };
132 | arguments: {
133 | kind: string;
134 | name: {
135 | kind: string;
136 | value: string;
137 | };
138 | value: {
139 | kind: string;
140 | value: string;
141 | };
142 | }[];
143 | selectionSet?: {
144 | kind: string;
145 | selections: typeof selections;
146 | };
147 | }[],
148 | queryArray: (string | string[])[] = [],
149 | queries: string[] = []
150 | ): string[] {
151 | // Looping through the selections to build the queries array
152 | for (const selection of selections) {
153 | // add current query to queryArray
154 | queryArray.push(selection.name.value);
155 | // if a query has arguments, format and add to query array
156 | if (selection.arguments?.length > 0) {
157 | const argumentArray: string[] = [];
158 |
159 | // looping through the arguments to add them to the argument array
160 | for (const argument of selection.arguments) {
161 | argumentArray.push(`${argument.name.value}:${argument.value.value}`);
162 | }
163 | queryArray.push([argumentArray.join(',')]);
164 | }
165 | // if there is a selectionSet property, there are more deeply nested queries
166 | if (selection.selectionSet) {
167 | // recursively invoke magniParser passing in selections array
168 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
169 | } else {
170 | let string = ``;
171 | // if query array looks like this: ['messageById', ['id:4'], 'message']
172 | // formated query will look like this: {allMessages(id:4){message}}
173 |
174 | // looping through the query array in reverse to build the query string
175 | for (let i = queryArray.length - 1; i >= 0; i--) {
176 | // arguments are put into an array and need to be formatted differently
177 | if (Array.isArray(queryArray[i])) {
178 | string = `(${queryArray[i][0]})${string}`;
179 | } else {
180 | string = `{${queryArray[i] + string}}`;
181 | }
182 | }
183 | // adding the completed query to the queries array
184 | queries.push(string);
185 | }
186 | // remove last element, as it's not wanted on next iteration
187 | queryArray.pop();
188 | }
189 | // returning the array of individual querys
190 | return queries;
191 | };
192 |
193 | // export default MagniClient;
194 |
195 | module.exports = MagniClient;
196 |
--------------------------------------------------------------------------------
/magnicache-client/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@magnicache/client",
3 | "version": "1.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@magnicache/client",
9 | "version": "1.0.1",
10 | "license": "MIT",
11 | "dependencies": {
12 | "lodash.mergewith": "^4.6.2"
13 | }
14 | },
15 | "node_modules/lodash.mergewith": {
16 | "version": "4.6.2",
17 | "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
18 | "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
19 | }
20 | },
21 | "dependencies": {
22 | "lodash.mergewith": {
23 | "version": "4.6.2",
24 | "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
25 | "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/magnicache-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@magnicache/client",
3 | "version": "1.0.1",
4 | "description": "Client-side caching layer for GraphQL",
5 | "main": "magnicache-client.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/oslabs-beta/MagniCache.git"
12 | },
13 | "author": "MagniCache",
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/oslabs-beta/MagniCache/issues"
17 | },
18 | "keywords": [
19 | "cache",
20 | "caching",
21 | "graphQL",
22 | "client",
23 | "clientside",
24 | "client-side",
25 | "lightweight",
26 | "localstorage",
27 | "magnicache",
28 | "magniClient",
29 | "query",
30 | "performance",
31 | "API",
32 | "optimization",
33 | "response time",
34 | "scalability"
35 | ],
36 | "homepage": "https://github.com/oslabs-beta/MagniCache#readme",
37 | "dependencies": {
38 | "lodash.mergewith": "^4.6.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/magnicache-demo/db-model.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 | const PG_URI =
3 | 'postgres://cnnwoeyo:KbB0LqwnuaMtc8mLx26otpKQCgEl823N@salt.db.elephantsql.com/cnnwoeyo';
4 |
5 | const pool = new Pool({
6 | connectionString: PG_URI,
7 | });
8 |
9 | module.exports = {
10 | query: (text, params, callback) => {
11 | // console.log(`executed query`, text);
12 | return pool.query(text, params, callback);
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/magnicache-demo/magnicache-server/IntrospectionQuery.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | IntrospectionQuery: `query IntrospectionQuery {
3 | __schema {
4 |
5 | queryType { name fields{name type{...TypeRef}} }
6 | mutationType { name fields{name type{...TypeRef}} }
7 | types {
8 | ...FullType
9 | }
10 | }
11 | }
12 |
13 | fragment FullType on __Type {
14 | kind
15 | name
16 | description
17 |
18 | fields(includeDeprecated: true) {
19 | name
20 | description
21 | args {
22 | ...InputValue
23 | }
24 | type {
25 | ...TypeRef
26 | }
27 | isDeprecated
28 | deprecationReason
29 | }
30 | inputFields {
31 | ...InputValue
32 | }
33 | interfaces {
34 | ...TypeRef
35 | }
36 | enumValues(includeDeprecated: true) {
37 | name
38 | description
39 | isDeprecated
40 | deprecationReason
41 | }
42 | possibleTypes {
43 | ...TypeRef
44 | }
45 | }
46 |
47 | fragment InputValue on __InputValue {
48 | name
49 | description
50 | type { ...TypeRef }
51 | defaultValue
52 |
53 |
54 | }
55 |
56 | fragment TypeRef on __Type {
57 | kind
58 | name
59 | ofType {
60 | kind
61 | name
62 | ofType {
63 | kind
64 | name
65 | ofType {
66 | kind
67 | name
68 | ofType {
69 | kind
70 | name
71 | ofType {
72 | kind
73 | name
74 | ofType {
75 | kind
76 | name
77 | ofType {
78 | kind
79 | name
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }`,
88 | };
89 |
--------------------------------------------------------------------------------
/magnicache-demo/magnicache-server/magnicache-server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | exports.__esModule = true;
3 | var _a = require('graphql'), GraphQLSchema = _a.GraphQLSchema, graphql = _a.graphql;
4 | var parse = require('graphql/language/parser').parse;
5 | // import * as mergeWith from 'lodash.mergewith';
6 | var mergeWith = require('lodash.mergewith');
7 | var IntrospectionQuery = require('./IntrospectionQuery').IntrospectionQuery;
8 | // TODO?: type "this" more specifically
9 | // TODO?: add query type to linked list => refactor mutations
10 | // TODO?: rename EvictionNode => Node
11 | // TODO?: put Cache.validate on Magnicache prototype, only store schema once
12 | function Magnicache(schema, maxSize) {
13 | if (maxSize === void 0) { maxSize = 100; }
14 | if (!this.schemaIsValid(schema)) {
15 | throw new Error('This schema is invalid. Please ensure that the passed in schema is an instance of GraphQLSchema, and that you are using a graphql package of version 14.0.0 or later');
16 | }
17 | // save the provided schema
18 | this.schema = schema;
19 | // max size of cache in atomized queries
20 | this.maxSize = maxSize;
21 | // instantiate doublely linked list to handle caching
22 | this.cache = new Cache(maxSize, schema);
23 | this.schemaTree = this.schemaParser(schema);
24 | this.metrics = {
25 | cacheUsage: 0,
26 | sizeLeft: this.maxSize,
27 | totalHits: 0,
28 | totalMisses: 0,
29 | AvgCacheTime: 0,
30 | AvgMissTime: 0,
31 | AvgMemAccTime: 0
32 | };
33 | // bind method(s)
34 | this.query = this.query.bind(this);
35 | this.schemaParser = this.schemaParser.bind(this);
36 | }
37 | // linked list node constructor
38 | var EvictionNode = /** @class */ (function () {
39 | function EvictionNode(key, value) {
40 | this.key = key;
41 | this.value = value;
42 | this.next = null;
43 | this.prev = null;
44 | }
45 | return EvictionNode;
46 | }());
47 | // linked list constructor
48 | var Cache = /** @class */ (function () {
49 | function Cache(maxSize, schema) {
50 | this.maxSize = maxSize;
51 | this.map = new Map();
52 | this.head = null;
53 | this.tail = null;
54 | this.schema = schema;
55 | // method binding
56 | this.create = this.create.bind(this);
57 | this["delete"] = this["delete"].bind(this);
58 | this.get = this.get.bind(this);
59 | this.includes = this.includes.bind(this);
60 | }
61 | // insert a node at the head of the list && if maxsize is reached, evict least recently used node
62 | Cache.prototype.create = function (key, value) {
63 | // Create a new node
64 | var newNode = new EvictionNode(key, value);
65 | // add node to map
66 | this.map.set(key, newNode);
67 | // if linked list is empty, set at head && tail
68 | if (!this.head) {
69 | this.head = newNode;
70 | this.tail = newNode;
71 | // otherwise add new node at head of list
72 | }
73 | else {
74 | newNode.next = this.head;
75 | this.head.prev = newNode;
76 | this.head = newNode;
77 | }
78 | // check cache size, prune tail if necessary
79 | if (this.map.size > this.maxSize)
80 | this["delete"](this.tail);
81 | return newNode;
82 | };
83 | // remove node from linked list
84 | // TODO: type node
85 | Cache.prototype["delete"] = function (node) {
86 | // SHOULD NEVER RUN: delete should only be invoked when node is known to exist
87 | if (node === null)
88 | throw new Error('ERROR in MagniCache.cache.delete: node is null');
89 | if (node.next === null && node.prev === null) {
90 | this.head = null;
91 | this.tail = null;
92 | // if node is at tail
93 | }
94 | else if (node.next === null) {
95 | node.prev.next = null;
96 | this.tail = node.prev;
97 | // if node is at head
98 | }
99 | else if (node.prev === null) {
100 | node.next.prev = null;
101 | this.head = node.next;
102 | // if node is not head of tail;
103 | }
104 | else {
105 | node.prev.next = node.next;
106 | node.next.prev = node.prev;
107 | }
108 | // remove node from map
109 | this.map["delete"](node.key);
110 | };
111 | // retrieve node from linked list
112 | Cache.prototype.get = function (key) {
113 | var node = this.map.get(key);
114 | // SHOULD NEVER RUN: get should only be invoked when node is known to exist
115 | if (node === null)
116 | throw new Error('ERROR in MagniCache.cache.get: node is null');
117 | // if the node is at the head, simply return the value
118 | if (node.prev === null)
119 | return node.value;
120 | // if node is at the tail, remove it from the tail
121 | else if (node.next === null) {
122 | this.tail = node.prev;
123 | node.prev.next = null;
124 | // if node is neither, remove it
125 | }
126 | else {
127 | node.prev.next = node.next;
128 | node.next.prev = node.prev;
129 | }
130 | // move the current head down one in the LL, move the current node to the head
131 | this.head.prev = node;
132 | node.prev = null;
133 | node.next = this.head;
134 | this.head = node;
135 | return node.value;
136 | };
137 | // TODO: type node
138 | Cache.prototype.validate = function (node) {
139 | var _this = this;
140 | if ((node === null || node === void 0 ? void 0 : node.key) === null)
141 | throw new Error('ERROR in MagniCache.cache.validate: invalid node');
142 | graphql({ schema: this.schema, source: node.key })
143 | .then(function (result) {
144 | if (!result.error) {
145 | node.value = result;
146 | }
147 | else {
148 | _this["delete"](node);
149 | }
150 | })["catch"](function (err) {
151 | throw new Error('ERROR in MagniCache.cache.validate: error executing graphql query');
152 | });
153 | return;
154 | };
155 | // syntactic sugar to check if cache has a key
156 | Cache.prototype.includes = function (key) {
157 | return this.map.has(key);
158 | };
159 | // syntactic sugar to get linked list length
160 | Cache.prototype.count = function () {
161 | return this.map.size;
162 | };
163 | return Cache;
164 | }());
165 | // used in middleware chain
166 | Magnicache.prototype.query = function (req, res, next) {
167 | // get graphql query from request body
168 | var _this = this;
169 | var query = req.body.query;
170 | // if query is null, send back a 400 code
171 | // if (query === null || query === '') {
172 | // res.locals.queryResponse = 'missing query body';
173 | // return next();
174 | // }
175 | //TODO: Make sure to handle the error from parse if it is an invalid query
176 | // parse the query into an AST
177 | // let ast;
178 | var ast;
179 | try {
180 | var parsedAst = parse(query).definitions[0];
181 | ast = parsedAst;
182 | }
183 | catch (error) {
184 | res.locals.queryResponse = 'Invalid query';
185 | return next();
186 | console.error('An error occurred while parsing the query:', error);
187 | }
188 | // if query is for 'clearCache', clear the cache and return next
189 | if (ast.selectionSet.selections[0].name.value === 'clearCache') {
190 | this.cache = new Cache(this.maxSize, this.schema);
191 | res.locals.queryResponse = { cacheStatus: 'cacheCleared' };
192 | return next();
193 | }
194 | // if query is for metrics, attach metrics to locals and return next
195 | if (ast.selectionSet.selections[0].name.value === 'getMetrics') {
196 | res.locals.queryResponse = this.metrics;
197 | return next();
198 | }
199 | // check if the operation type is a query
200 | if (ast.operation === 'query') {
201 | // if query is for the schema, bypass cache and execute query as normal
202 | if (ast.selectionSet.selections[0].name.value === '__schema') {
203 | graphql({ schema: this.schema, source: query })
204 | .then(function (result) {
205 | res.locals.queryResponse = result;
206 | return next();
207 | })["catch"](function (err) {
208 | throw new Error('ERROR executing graphql query' + JSON.stringify(err));
209 | });
210 | }
211 | else {
212 | // parse the ast into usable graphql querys
213 | var queries_2 = this.magniParser(ast.selectionSet.selections);
214 | var queryResponses_1 = [];
215 | // compile all queryResponses into one object that is return to requester
216 | var compileQueries_1 = function () {
217 | // merge all responses into one object
218 | var response = {};
219 | for (var _i = 0, queryResponses_2 = queryResponses_1; _i < queryResponses_2.length; _i++) {
220 | var queryResponse = queryResponses_2[_i];
221 | response = mergeWith(response, queryResponse);
222 | }
223 | // assign the combined result to the response locals
224 | res.locals.queryResponse = response;
225 | return next();
226 | };
227 | // calculate the average memory access time
228 | var calcAMAT_1 = function () {
229 | // calculate cache hit rate
230 | var hitRate = _this.metrics.totalHits /
231 | (_this.metrics.totalHits + _this.metrics.totalMisses);
232 | // calculate average memory access time and update metrics object
233 | _this.metrics.AvgMemAccTime = Math.round(hitRate * _this.metrics.AvgCacheTime +
234 | (1 - hitRate) * _this.metrics.AvgMissTime);
235 | // Return the calculated metric
236 | return _this.metrics.AvgMemAccTime;
237 | };
238 | var _loop_1 = function (query_1) {
239 | // check if query is already cached
240 | if (this_1.cache.includes(query_1)) {
241 | // add a cookie indicating that the cache was hit
242 | // will be overwritten by any following querys
243 | res.cookie('cacheStatus', 'hit');
244 | // update the metrics with a hit count
245 | this_1.metrics.totalHits++;
246 | // Start a timter
247 | var hitStart = Date.now();
248 | // retrieve the data from cache and add to queryResponses array
249 | queryResponses_1.push(this_1.cache.get(query_1));
250 | // check if all queries have been fetched
251 | if (queries_2.length === queryResponses_1.length) {
252 | // compile all queries
253 | compileQueries_1();
254 | }
255 | // calculate the hit time
256 | var hitTime = Math.floor(Date.now() - hitStart);
257 | // update the metrics object
258 | this_1.metrics.AvgCacheTime = Math.round((this_1.metrics.AvgCacheTime + hitTime) / this_1.metrics.totalHits);
259 | }
260 | else {
261 | // start the miss timer
262 | var missStart_1 = Date.now();
263 | // execute the query
264 | graphql({ schema: this_1.schema, source: query_1 })
265 | .then(function (result) {
266 | // if no error, cache response
267 | if (!result.err)
268 | _this.cache.create(query_1, result);
269 | // update the metrics with the new size
270 | _this.metrics.cacheUsage = _this.cache.count();
271 | // store the query response
272 | queryResponses_1.push(result);
273 | })
274 | // update miss time as well as update the average memory access time
275 | .then(function () {
276 | // add a cookie indicating that the cache was missed
277 | res.cookie('cacheStatus', 'miss');
278 | // update the metrics with a missCount
279 | _this.metrics.totalMisses++;
280 | _this.sizeLeft = _this.maxSize - _this.metrics.cacheUsage;
281 | var missTime = Date.now() - missStart_1;
282 | _this.metrics.AvgMissTime = Math.round((_this.metrics.AvgMissTime + missTime) / _this.metrics.totalMisses);
283 | _this.metrics.AvgMissTime == Math.round(_this.metrics.AvgMissTime);
284 | calcAMAT_1();
285 | // check if all queries have been fetched
286 | if (queries_2.length === queryResponses_1.length) {
287 | // compile all queries
288 | compileQueries_1();
289 | }
290 | })["catch"](function (err) {
291 | throw new Error('ERROR executing graphql query' + JSON.stringify(err));
292 | });
293 | }
294 | };
295 | var this_1 = this;
296 | // loop through the individual queries and execute them in turn
297 | for (var _i = 0, queries_1 = queries_2; _i < queries_1.length; _i++) {
298 | var query_1 = queries_1[_i];
299 | _loop_1(query_1);
300 | }
301 | }
302 | // if not a query
303 | }
304 | else if (ast.operation === 'mutation') {
305 | // first execute mutation normally
306 | graphql({ schema: this.schema, source: query })
307 | .then(function (result) {
308 | res.locals.queryResponse = result;
309 | return next();
310 | })
311 | .then(function () {
312 | // get all mutation types, utilizing a set to avoid duplicates
313 | var mutationTypes = new Set();
314 | for (var _i = 0, _a = ast.selectionSet.selections; _i < _a.length; _i++) {
315 | var mutation = _a[_i];
316 | var mutationName = mutation.name.value;
317 | mutationTypes.add(_this.schemaTree.mutations[mutationName]);
318 | }
319 | // for every mutation type, get every corresponding query type
320 | mutationTypes.forEach(function (mutationType) {
321 | var userQueries = new Set();
322 | for (var query_2 in _this.schemaTree.queries) {
323 | var type = _this.schemaTree.queries[query_2];
324 | if (mutationType === type)
325 | userQueries.add(query_2);
326 | }
327 | userQueries.forEach(function (query) {
328 | for (var currentNode = _this.cache.head; currentNode !== null; currentNode = currentNode.next) {
329 | if (currentNode.key.includes(query)) {
330 | _this.cache.validate(currentNode);
331 | }
332 | }
333 | });
334 | });
335 | })["catch"](function (err) {
336 | console.error(err);
337 | return err;
338 | });
339 | }
340 | };
341 | // invoked with AST as an argument, returns an array of graphql schemas
342 | Magnicache.prototype.magniParser = function (selections, queryArray, queries) {
343 | if (queryArray === void 0) { queryArray = []; }
344 | if (queries === void 0) { queries = []; }
345 | // Looping through the selections to build the queries array
346 | for (var _i = 0, selections_1 = selections; _i < selections_1.length; _i++) {
347 | var selection = selections_1[_i];
348 | // add current query to queryArray
349 | queryArray.push(selection.name.value);
350 | // if a query has arguments, format and add to query array
351 | if (selection.arguments.length > 0) {
352 | var argumentArray = [];
353 | // looping through the arguments to add them to the argument array
354 | for (var _a = 0, _b = selection.arguments; _a < _b.length; _a++) {
355 | var argument = _b[_a];
356 | argumentArray.push("".concat(argument.name.value, ":").concat(argument.value.value));
357 | }
358 | queryArray.push([argumentArray.join(',')]);
359 | }
360 | // if there is a selectionSet property, there are more deeply nested queries
361 | if (selection.selectionSet) {
362 | // recursively invoke magniParser passing in selections array
363 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
364 | }
365 | else {
366 | var queryString = "";
367 | // if query array looks like this: ['messageById', ['id:4'], 'message']
368 | // formated query will look like this: {allMessages(id:4){message}}
369 | // looping through the query array in reverse to build the query string
370 | for (var i = queryArray.length - 1; i >= 0; i--) {
371 | // arguments are put into an array and need to be formatted differently
372 | if (Array.isArray(queryArray[i])) {
373 | queryString = "(".concat(queryArray[i][0], ")").concat(queryString);
374 | }
375 | else {
376 | queryString = "{".concat(queryArray[i] + queryString, "}");
377 | }
378 | }
379 | // adding the completed query to the queries array
380 | queries.push(queryString);
381 | }
382 | // remove last element, as it's not wanted on next iteration
383 | queryArray.pop();
384 | }
385 | // returning the array of individual querys
386 | return queries;
387 | };
388 | Magnicache.prototype.schemaParser = function (schema) {
389 | // TODO: refactor to be able to store multiple types for each query
390 | // TODO: stricter types for schemaTree
391 | var schemaTree = {
392 | queries: {
393 | //Ex: allMessages:Messages
394 | },
395 | mutations: {}
396 | };
397 | // TODO: type 'type'
398 | // TODO: refactor to ensure there isn't an infinite loop
399 | var typeFinder = function (type) {
400 | // console.log('field', type);
401 | if (type.name === null)
402 | return typeFinder(type.ofType);
403 | return type.name;
404 | };
405 | // TODO: Type the result for the schema
406 | graphql({ schema: this.schema, source: IntrospectionQuery })
407 | .then(function (result) {
408 | // console.log(result.data.__schema.queryType);
409 | if (result.data.__schema.queryType) {
410 | for (var _i = 0, _a = result.data.__schema.queryType.fields; _i < _a.length; _i++) {
411 | var field = _a[_i];
412 | schemaTree.queries[field.name] = typeFinder(field.type);
413 | }
414 | }
415 | if (result.data.__schema.mutationType) {
416 | for (var _b = 0, _c = result.data.__schema.mutationType.fields; _b < _c.length; _b++) {
417 | var field = _c[_b];
418 | schemaTree.mutations[field.name] = typeFinder(field.type);
419 | }
420 | }
421 | })
422 | .then(function () {
423 | // console.log('schemaTree', schemaTree);
424 | })["catch"](function (err) {
425 | console.error(err);
426 | // throw new Error(`ERROR executing graphql query` + JSON.stringify(err));
427 | return err;
428 | });
429 | return schemaTree;
430 | };
431 | Magnicache.prototype.schemaIsValid = function (schema) {
432 | return schema instanceof GraphQLSchema;
433 | };
434 | module.exports = Magnicache;
435 |
--------------------------------------------------------------------------------
/magnicache-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "magnicache-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js",
9 | "dev": "nodemon server.js",
10 | "tsc": "tsc --watch --outDir ../magnicache-demo ../magnicache-server/magnicache-server.ts"
11 | },
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "lodash.mergewith": "^4.6.2",
16 | "@magnicache/server": "^1.0.9",
17 | "express": "^4.18.2",
18 | "graphql": "^16.6.0",
19 | "pg": "^8.9.0"
20 | },
21 | "devDependencies": {
22 | "nodemon": "^2.0.20",
23 | "typescript": "^4.9.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/magnicache-demo/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const app = express();
4 | const db = require('./db-model.js');
5 | const MagniCache = require('./magnicache-server/magnicache-server.js');
6 | // const MagniCache = require('@magnicache/server');
7 |
8 | const {
9 | GraphQLNonNull,
10 | GraphQLInt,
11 | GraphQLList,
12 | GraphQLString,
13 | GraphQLBoolean,
14 | GraphQLSchema,
15 | GraphQLObjectType,
16 | } = require('graphql');
17 |
18 | const PORT = 3000;
19 |
20 | app.use(express.json());
21 | // app.use(cookieParser());
22 |
23 | app.get(['/', '/demo', '/info', '/team'], (req, res) => {
24 | // express.static(path.join(__dirname, '../client'));
25 | res.status(200).sendFile(path.join(__dirname, '../client/index.html'));
26 | });
27 |
28 | app.use(express.static(path.join(__dirname, '../build')));
29 |
30 | const UserType = new GraphQLObjectType({
31 | name: 'User',
32 | description: 'type of Users',
33 | fields: () => ({
34 | user_id: { type: GraphQLInt },
35 | username: { type: GraphQLString },
36 | password: { type: GraphQLString },
37 | }),
38 | });
39 |
40 | const MessageType = new GraphQLObjectType({
41 | name: 'Message',
42 | description: 'type Message',
43 | fields: () => ({
44 | message: { type: GraphQLString },
45 | message_id: { type: GraphQLInt },
46 | sender_id: { type: GraphQLInt },
47 | time: { type: GraphQLString, resolve: () => new Date().toDateString() },
48 | human: { type: GraphQLString },
49 | user: {
50 | type: UserType,
51 | resolve: async (message) => {
52 | const { user_id, username, password, email } = message;
53 | return { user_id, username, password, email };
54 | },
55 | },
56 | }),
57 | });
58 |
59 | //chain queries tg w ; ? -- > TRUE MAN comment
60 |
61 | const RootQueryType = new GraphQLObjectType({
62 | name: 'Query',
63 | description: 'Root Query',
64 | fields: () => ({
65 | allMessages: {
66 | type: new GraphQLList(MessageType),
67 | description: 'all the messages',
68 | resolve: async (parent) => {
69 | // console.log('parent', parent);
70 | const query =
71 | 'SELECT m.*, users.* FROM messages m INNER JOIN users ON users.user_id = m.sender_id';
72 | const data = await db.query(query);
73 | return data.rows;
74 | },
75 | },
76 | messageById: {
77 | type: new GraphQLList(MessageType),
78 | description: ' ',
79 | args: {
80 | id: { type: GraphQLInt },
81 | },
82 | resolve: async (parent, args) => {
83 | const value = [args.id];
84 | const query =
85 | 'SELECT m.*, users.* FROM messages m INNER JOIN users ON users.user_id = m.sender_id WHERE message_id=$1';
86 | const data = await db.query(query, value);
87 | return data.rows;
88 | },
89 | },
90 | }),
91 | });
92 |
93 | const RootMutationType = new GraphQLObjectType({
94 | name: 'Mutation',
95 | description: 'Mutates Messages',
96 | fields: () => ({
97 | addMessage: {
98 | type: MessageType,
99 | description: 'add a message to the db',
100 | args: {
101 | sender_id: { type: GraphQLInt },
102 | message: { type: GraphQLString },
103 | },
104 | resolve: async (parent, args) => {
105 | const message = {
106 | sender_id: args.sender_id,
107 | message: args.message,
108 | };
109 | const value = [args.message, args.sender_id];
110 | const query =
111 | 'INSERT INTO messages (sender_id,message) VALUES ($2, $1) RETURNING *;';
112 | const data = await db.query(query, value);
113 | return data.rows[0];
114 | },
115 | },
116 | }),
117 | });
118 |
119 | const schema = new GraphQLSchema({
120 | query: RootQueryType,
121 | mutation: RootMutationType,
122 | });
123 |
124 | const magnicache = new MagniCache(schema);
125 |
126 | //ideally, we would want a viz to have the following middleware:
127 | //app.get(/magnicache, magnicache.viz, (req,res) => {
128 | //return res.status(200).sendFile()
129 | //})
130 |
131 | //currently, we want any requests being sent to /graphql to come back w a custom (header/cookie?) that shows if it is chached or not
132 | //
133 | //alternatively, we can have magnicache.query take a vizaulaizer options, set and the send the respoinser from the middleware
134 | app.use('/graphql', magnicache.query, (req, res) => {
135 | return res.status(200).send(res.locals.queryResponse);
136 | });
137 |
138 | //catch-all route
139 | app.use('/', (req, res, next) =>
140 | //TODO add a 404 page to route to
141 | next({
142 | log: 'Express catch all handler caught unknown route',
143 | status: 404,
144 | message: { err: 'Route not found' },
145 | })
146 | );
147 |
148 | const defaultErr = {
149 | log: 'Express error handler caught an unknown middleware error',
150 | status: 400,
151 | message: { err: 'An error occurred' },
152 | };
153 |
154 | app.use((err, req, res, next) => {
155 | const errorObj = Object.assign(defaultErr, err);
156 | // console.log(errorObj.log);
157 | return res.status(errorObj.status).json(errorObj.message);
158 | });
159 |
160 | if (process.env.NODE_ENV !== 'test') {
161 | app.listen(PORT, () => {
162 | console.log(`Server listening on PORT ${PORT}`);
163 | });
164 | }
165 |
166 | module.exports = app;
167 |
--------------------------------------------------------------------------------
/magnicache-demo/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | exports.__esModule = true;
3 | /*
4 | this.metrics = { //type this to be a cacheMetrics type
5 | cacheUsage: 0,
6 | sizeLeft: this.maxSize,
7 | totalHits: 0,
8 | totalMisses: 0,
9 | AvgCacheTime: 0, //for atomic queries only, can change to query as a whole later on
10 | AvgMissTime: 0,
11 | AvgMemAccTime: 0, // hit rate * cacheTime + miss rate * missTIme
12 | };
13 | */
14 |
--------------------------------------------------------------------------------
/magnicache-server/IntrospectionQuery.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | IntrospectionQuery: `query IntrospectionQuery {
3 | __schema {
4 |
5 | queryType { name fields{name type{...TypeRef}} }
6 | mutationType { name fields{name type{...TypeRef}} }
7 | types {
8 | ...FullType
9 | }
10 | }
11 | }
12 |
13 | fragment FullType on __Type {
14 | kind
15 | name
16 | description
17 |
18 | fields(includeDeprecated: true) {
19 | name
20 | description
21 | args {
22 | ...InputValue
23 | }
24 | type {
25 | ...TypeRef
26 | }
27 | isDeprecated
28 | deprecationReason
29 | }
30 | inputFields {
31 | ...InputValue
32 | }
33 | interfaces {
34 | ...TypeRef
35 | }
36 | enumValues(includeDeprecated: true) {
37 | name
38 | description
39 | isDeprecated
40 | deprecationReason
41 | }
42 | possibleTypes {
43 | ...TypeRef
44 | }
45 | }
46 |
47 | fragment InputValue on __InputValue {
48 | name
49 | description
50 | type { ...TypeRef }
51 | defaultValue
52 |
53 |
54 | }
55 |
56 | fragment TypeRef on __Type {
57 | kind
58 | name
59 | ofType {
60 | kind
61 | name
62 | ofType {
63 | kind
64 | name
65 | ofType {
66 | kind
67 | name
68 | ofType {
69 | kind
70 | name
71 | ofType {
72 | kind
73 | name
74 | ofType {
75 | kind
76 | name
77 | ofType {
78 | kind
79 | name
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }`,
88 | };
89 |
--------------------------------------------------------------------------------
/magnicache-server/README.md:
--------------------------------------------------------------------------------
1 | # Magnicache/server
2 |
3 | @magnicache/server is a lightweight GraphQL caching solution.
4 |
5 | Imported as a package from npm, @magnicache/server can be inserted into the middleware chain for a GraphQL endpoint to intercept GraphQL requests and scan the cache for previously executed queries.
6 |
7 |
8 | Queries present in the cache will return the cached result to the client, improving response speeds and overall GraphQL performance.
9 |
10 |
11 |
12 | ## How to use @magnicache/server in your GraphQL api
13 |
14 | 1. Install MagniCache Server.
15 |
16 | ```bash
17 | npm i @magnicache/server
18 | ```
19 |
20 | 2. Import MagniCache.
21 |
22 | ```js
23 | const MagniCache = require('@magnicache/server');
24 | ```
25 |
26 | 3. Declare a new instance of MagniCache, passing in your GraphQL schema.
27 |
28 | ```js
29 | const magnicache = new MagniCache(schema);
30 | ```
31 |
32 | 4. Insert magnicache.query into the middleware chain for your '/graphql' route.
33 |
34 | - Ensure all request bodies are parsed
35 |
36 | ```js
37 | app.use(express.json());
38 |
39 | app.use('/graphql', magnicache.query, (req, res) =>
40 | res.status(200).send(res.locals.queryResponse)
41 | );
42 | ```
43 |
44 | ## Contributors
45 |
46 | [Ahmed Chami](https://www.linkedin.com/in/ahmed-chami/)
47 |
48 | [Aria Soltankhah](https://www.linkedin.com/in/ariasol/)
49 |
50 | [Truman Miller](https://www.linkedin.com/in/truman-miller)
51 |
52 | [Yousuf Elkhoga](https://www.linkedin.com/in/yousufelkhoga/)
53 |
--------------------------------------------------------------------------------
/magnicache-server/db/index.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require("pg");
2 | const connectionString =
3 | "postgres://cinyujcx:lJx4kqZRMvoIVg12Z8WcnKSojzfDQPUt@isilo.db.elephantsql.com/cinyujcx";
4 | const pool = new Pool({
5 | connectionString,
6 | max: 5,
7 | });
8 |
9 | module.exports = {
10 | query: (text, params, callback) => {
11 | console.log("executed query", text);
12 | return pool.query(text, params, callback);
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/magnicache-server/magnicache-server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | exports.__esModule = true;
3 | var _a = require('graphql'), GraphQLSchema = _a.GraphQLSchema, graphql = _a.graphql;
4 | var parse = require('graphql/language/parser').parse;
5 | // import * as mergeWith from 'lodash.mergewith';
6 | var mergeWith = require('lodash.mergewith');
7 | var IntrospectionQuery = require('./IntrospectionQuery').IntrospectionQuery;
8 | // TODO?: type "this" more specifically
9 | // TODO?: add query type to linked list => refactor mutations
10 | // TODO?: rename EvictionNode => Node
11 | // TODO?: put Cache.validate on Magnicache prototype, only store schema once
12 | function Magnicache(schema, maxSize) {
13 | if (maxSize === void 0) { maxSize = 100; }
14 | if (!this.schemaIsValid(schema)) {
15 | throw new Error('This schema is invalid. Please ensure that the passed in schema is an instance of GraphQLSchema, and that you are using a graphql package of version 14.0.0 or later');
16 | }
17 | // save the provided schema
18 | this.schema = schema;
19 | // max size of cache in atomized queries
20 | this.maxSize = maxSize;
21 | // instantiate doublely linked list to handle caching
22 | this.cache = new Cache(maxSize, schema);
23 | this.schemaTree = this.schemaParser(schema);
24 | this.metrics = {
25 | cacheUsage: 0,
26 | sizeLeft: this.maxSize,
27 | totalHits: 0,
28 | totalMisses: 0,
29 | AvgCacheTime: 0,
30 | AvgMissTime: 0,
31 | AvgMemAccTime: 0
32 | };
33 | // bind method(s)
34 | this.query = this.query.bind(this);
35 | this.schemaParser = this.schemaParser.bind(this);
36 | }
37 | // linked list node constructor
38 | var EvictionNode = /** @class */ (function () {
39 | function EvictionNode(key, value) {
40 | this.key = key;
41 | this.value = value;
42 | this.next = null;
43 | this.prev = null;
44 | }
45 | return EvictionNode;
46 | }());
47 | // linked list constructor
48 | var Cache = /** @class */ (function () {
49 | function Cache(maxSize, schema) {
50 | this.maxSize = maxSize;
51 | this.map = new Map();
52 | this.head = null;
53 | this.tail = null;
54 | this.schema = schema;
55 | // method binding
56 | this.create = this.create.bind(this);
57 | this["delete"] = this["delete"].bind(this);
58 | this.get = this.get.bind(this);
59 | this.includes = this.includes.bind(this);
60 | }
61 | // insert a node at the head of the list && if maxsize is reached, evict least recently used node
62 | Cache.prototype.create = function (key, value) {
63 | // Create a new node
64 | var newNode = new EvictionNode(key, value);
65 | // add node to map
66 | this.map.set(key, newNode);
67 | // if linked list is empty, set at head && tail
68 | if (!this.head) {
69 | this.head = newNode;
70 | this.tail = newNode;
71 | // otherwise add new node at head of list
72 | }
73 | else {
74 | newNode.next = this.head;
75 | this.head.prev = newNode;
76 | this.head = newNode;
77 | }
78 | // check cache size, prune tail if necessary
79 | if (this.map.size > this.maxSize)
80 | this["delete"](this.tail);
81 | return newNode;
82 | };
83 | // remove node from linked list
84 | // TODO: type node
85 | Cache.prototype["delete"] = function (node) {
86 | // SHOULD NEVER RUN: delete should only be invoked when node is known to exist
87 | if (node === null)
88 | throw new Error('ERROR in MagniCache.cache.delete: node is null');
89 | if (node.next === null && node.prev === null) {
90 | this.head = null;
91 | this.tail = null;
92 | // if node is at tail
93 | }
94 | else if (node.next === null) {
95 | node.prev.next = null;
96 | this.tail = node.prev;
97 | // if node is at head
98 | }
99 | else if (node.prev === null) {
100 | node.next.prev = null;
101 | this.head = node.next;
102 | // if node is not head of tail;
103 | }
104 | else {
105 | node.prev.next = node.next;
106 | node.next.prev = node.prev;
107 | }
108 | // remove node from map
109 | this.map["delete"](node.key);
110 | };
111 | // retrieve node from linked list
112 | Cache.prototype.get = function (key) {
113 | var node = this.map.get(key);
114 | // SHOULD NEVER RUN: get should only be invoked when node is known to exist
115 | if (node === null)
116 | throw new Error('ERROR in MagniCache.cache.get: node is null');
117 | // if the node is at the head, simply return the value
118 | if (node.prev === null)
119 | return node.value;
120 | // if node is at the tail, remove it from the tail
121 | else if (node.next === null) {
122 | this.tail = node.prev;
123 | node.prev.next = null;
124 | // if node is neither, remove it
125 | }
126 | else {
127 | node.prev.next = node.next;
128 | node.next.prev = node.prev;
129 | }
130 | // move the current head down one in the LL, move the current node to the head
131 | this.head.prev = node;
132 | node.prev = null;
133 | node.next = this.head;
134 | this.head = node;
135 | return node.value;
136 | };
137 | // TODO: type node
138 | Cache.prototype.validate = function (node) {
139 | var _this = this;
140 | if ((node === null || node === void 0 ? void 0 : node.key) === null)
141 | throw new Error('ERROR in MagniCache.cache.validate: invalid node');
142 | graphql({ schema: this.schema, source: node.key })
143 | .then(function (result) {
144 | if (!result.error) {
145 | node.value = result;
146 | }
147 | else {
148 | _this["delete"](node);
149 | }
150 | })["catch"](function (err) {
151 | throw new Error('ERROR in MagniCache.cache.validate: error executing graphql query');
152 | });
153 | return;
154 | };
155 | // syntactic sugar to check if cache has a key
156 | Cache.prototype.includes = function (key) {
157 | return this.map.has(key);
158 | };
159 | // syntactic sugar to get linked list length
160 | Cache.prototype.count = function () {
161 | return this.map.size;
162 | };
163 | return Cache;
164 | }());
165 | // used in middleware chain
166 | Magnicache.prototype.query = function (req, res, next) {
167 | // get graphql query from request body
168 | var _this = this;
169 | var query = req.body.query;
170 | // if query is null, send back a 400 code
171 | // if (query === null || query === '') {
172 | // res.locals.queryResponse = 'missing query body';
173 | // return next();
174 | // }
175 | //TODO: Make sure to handle the error from parse if it is an invalid query
176 | // parse the query into an AST
177 | // let ast;
178 | var ast;
179 | try {
180 | var parsedAst = parse(query).definitions[0];
181 | ast = parsedAst;
182 | }
183 | catch (error) {
184 | res.locals.queryResponse = 'Invalid query';
185 | return next();
186 | console.error('An error occurred while parsing the query:', error);
187 | }
188 | // if query is for 'clearCache', clear the cache and return next
189 | if (ast.selectionSet.selections[0].name.value === 'clearCache') {
190 | this.cache = new Cache(this.maxSize, this.schema);
191 | res.locals.queryResponse = { cacheStatus: 'cacheCleared' };
192 | return next();
193 | }
194 | // if query is for metrics, attach metrics to locals and return next
195 | if (ast.selectionSet.selections[0].name.value === 'getMetrics') {
196 | res.locals.queryResponse = this.metrics;
197 | return next();
198 | }
199 | // check if the operation type is a query
200 | if (ast.operation === 'query') {
201 | // if query is for the schema, bypass cache and execute query as normal
202 | if (ast.selectionSet.selections[0].name.value === '__schema') {
203 | graphql({ schema: this.schema, source: query })
204 | .then(function (result) {
205 | res.locals.queryResponse = result;
206 | return next();
207 | })["catch"](function (err) {
208 | throw new Error('ERROR executing graphql query' + JSON.stringify(err));
209 | });
210 | }
211 | else {
212 | // parse the ast into usable graphql querys
213 | var queries_2 = this.magniParser(ast.selectionSet.selections);
214 | var queryResponses_1 = [];
215 | // compile all queryResponses into one object that is return to requester
216 | var compileQueries_1 = function () {
217 | // merge all responses into one object
218 | var response = {};
219 | for (var _i = 0, queryResponses_2 = queryResponses_1; _i < queryResponses_2.length; _i++) {
220 | var queryResponse = queryResponses_2[_i];
221 | response = mergeWith(response, queryResponse);
222 | }
223 | // assign the combined result to the response locals
224 | res.locals.queryResponse = response;
225 | return next();
226 | };
227 | // calculate the average memory access time
228 | var calcAMAT_1 = function () {
229 | // calculate cache hit rate
230 | var hitRate = _this.metrics.totalHits /
231 | (_this.metrics.totalHits + _this.metrics.totalMisses);
232 | // calculate average memory access time and update metrics object
233 | _this.metrics.AvgMemAccTime = Math.round(hitRate * _this.metrics.AvgCacheTime +
234 | (1 - hitRate) * _this.metrics.AvgMissTime);
235 | // Return the calculated metric
236 | return _this.metrics.AvgMemAccTime;
237 | };
238 | var _loop_1 = function (query_1) {
239 | // check if query is already cached
240 | if (this_1.cache.includes(query_1)) {
241 | // add a cookie indicating that the cache was hit
242 | // will be overwritten by any following querys
243 | res.cookie('cacheStatus', 'hit');
244 | // update the metrics with a hit count
245 | this_1.metrics.totalHits++;
246 | // Start a timter
247 | var hitStart = Date.now();
248 | // retrieve the data from cache and add to queryResponses array
249 | queryResponses_1.push(this_1.cache.get(query_1));
250 | // check if all queries have been fetched
251 | if (queries_2.length === queryResponses_1.length) {
252 | // compile all queries
253 | compileQueries_1();
254 | }
255 | // calculate the hit time
256 | var hitTime = Math.floor(Date.now() - hitStart);
257 | // update the metrics object
258 | this_1.metrics.AvgCacheTime = Math.round((this_1.metrics.AvgCacheTime + hitTime) / this_1.metrics.totalHits);
259 | }
260 | else {
261 | // start the miss timer
262 | var missStart_1 = Date.now();
263 | // execute the query
264 | graphql({ schema: this_1.schema, source: query_1 })
265 | .then(function (result) {
266 | // if no error, cache response
267 | if (!result.err)
268 | _this.cache.create(query_1, result);
269 | // update the metrics with the new size
270 | _this.metrics.cacheUsage = _this.cache.count();
271 | // store the query response
272 | queryResponses_1.push(result);
273 | })
274 | // update miss time as well as update the average memory access time
275 | .then(function () {
276 | // add a cookie indicating that the cache was missed
277 | res.cookie('cacheStatus', 'miss');
278 | // update the metrics with a missCount
279 | _this.metrics.totalMisses++;
280 | _this.sizeLeft = _this.maxSize - _this.metrics.cacheUsage;
281 | var missTime = Date.now() - missStart_1;
282 | _this.metrics.AvgMissTime = Math.round((_this.metrics.AvgMissTime + missTime) / _this.metrics.totalMisses);
283 | _this.metrics.AvgMissTime == Math.round(_this.metrics.AvgMissTime);
284 | calcAMAT_1();
285 | // check if all queries have been fetched
286 | if (queries_2.length === queryResponses_1.length) {
287 | // compile all queries
288 | compileQueries_1();
289 | }
290 | })["catch"](function (err) {
291 | throw new Error('ERROR executing graphql query' + JSON.stringify(err));
292 | });
293 | }
294 | };
295 | var this_1 = this;
296 | // loop through the individual queries and execute them in turn
297 | for (var _i = 0, queries_1 = queries_2; _i < queries_1.length; _i++) {
298 | var query_1 = queries_1[_i];
299 | _loop_1(query_1);
300 | }
301 | }
302 | // if not a query
303 | }
304 | else if (ast.operation === 'mutation') {
305 | // first execute mutation normally
306 | graphql({ schema: this.schema, source: query })
307 | .then(function (result) {
308 | res.locals.queryResponse = result;
309 | return next();
310 | })
311 | .then(function () {
312 | // get all mutation types, utilizing a set to avoid duplicates
313 | var mutationTypes = new Set();
314 | for (var _i = 0, _a = ast.selectionSet.selections; _i < _a.length; _i++) {
315 | var mutation = _a[_i];
316 | var mutationName = mutation.name.value;
317 | mutationTypes.add(_this.schemaTree.mutations[mutationName]);
318 | }
319 | // for every mutation type, get every corresponding query type
320 | mutationTypes.forEach(function (mutationType) {
321 | var userQueries = new Set();
322 | for (var query_2 in _this.schemaTree.queries) {
323 | var type = _this.schemaTree.queries[query_2];
324 | if (mutationType === type)
325 | userQueries.add(query_2);
326 | }
327 | userQueries.forEach(function (query) {
328 | for (var currentNode = _this.cache.head; currentNode !== null; currentNode = currentNode.next) {
329 | if (currentNode.key.includes(query)) {
330 | _this.cache.validate(currentNode);
331 | }
332 | }
333 | });
334 | });
335 | })["catch"](function (err) {
336 | console.error(err);
337 | return err;
338 | });
339 | }
340 | };
341 | // invoked with AST as an argument, returns an array of graphql schemas
342 | Magnicache.prototype.magniParser = function (selections, queryArray, queries) {
343 | if (queryArray === void 0) { queryArray = []; }
344 | if (queries === void 0) { queries = []; }
345 | // Looping through the selections to build the queries array
346 | for (var _i = 0, selections_1 = selections; _i < selections_1.length; _i++) {
347 | var selection = selections_1[_i];
348 | // add current query to queryArray
349 | queryArray.push(selection.name.value);
350 | // if a query has arguments, format and add to query array
351 | if (selection.arguments.length > 0) {
352 | var argumentArray = [];
353 | // looping through the arguments to add them to the argument array
354 | for (var _a = 0, _b = selection.arguments; _a < _b.length; _a++) {
355 | var argument = _b[_a];
356 | argumentArray.push("".concat(argument.name.value, ":").concat(argument.value.value));
357 | }
358 | queryArray.push([argumentArray.join(',')]);
359 | }
360 | // if there is a selectionSet property, there are more deeply nested queries
361 | if (selection.selectionSet) {
362 | // recursively invoke magniParser passing in selections array
363 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
364 | }
365 | else {
366 | var queryString = "";
367 | // if query array looks like this: ['messageById', ['id:4'], 'message']
368 | // formated query will look like this: {allMessages(id:4){message}}
369 | // looping through the query array in reverse to build the query string
370 | for (var i = queryArray.length - 1; i >= 0; i--) {
371 | // arguments are put into an array and need to be formatted differently
372 | if (Array.isArray(queryArray[i])) {
373 | queryString = "(".concat(queryArray[i][0], ")").concat(queryString);
374 | }
375 | else {
376 | queryString = "{".concat(queryArray[i] + queryString, "}");
377 | }
378 | }
379 | // adding the completed query to the queries array
380 | queries.push(queryString);
381 | }
382 | // remove last element, as it's not wanted on next iteration
383 | queryArray.pop();
384 | }
385 | // returning the array of individual querys
386 | return queries;
387 | };
388 | Magnicache.prototype.schemaParser = function (schema) {
389 | // TODO: refactor to be able to store multiple types for each query
390 | // TODO: stricter types for schemaTree
391 | var schemaTree = {
392 | queries: {
393 | //Ex: allMessages:Messages
394 | },
395 | mutations: {}
396 | };
397 | // TODO: type 'type'
398 | // TODO: refactor to ensure there isn't an infinite loop
399 | var typeFinder = function (type) {
400 | // console.log('field', type);
401 | if (type.name === null)
402 | return typeFinder(type.ofType);
403 | return type.name;
404 | };
405 | // TODO: Type the result for the schema
406 | graphql({ schema: this.schema, source: IntrospectionQuery })
407 | .then(function (result) {
408 | // console.log(result.data.__schema.queryType);
409 | if (result.data.__schema.queryType) {
410 | for (var _i = 0, _a = result.data.__schema.queryType.fields; _i < _a.length; _i++) {
411 | var field = _a[_i];
412 | schemaTree.queries[field.name] = typeFinder(field.type);
413 | }
414 | }
415 | if (result.data.__schema.mutationType) {
416 | for (var _b = 0, _c = result.data.__schema.mutationType.fields; _b < _c.length; _b++) {
417 | var field = _c[_b];
418 | schemaTree.mutations[field.name] = typeFinder(field.type);
419 | }
420 | }
421 | })
422 | .then(function () {
423 | // console.log('schemaTree', schemaTree);
424 | })["catch"](function (err) {
425 | console.error(err);
426 | // throw new Error(`ERROR executing graphql query` + JSON.stringify(err));
427 | return err;
428 | });
429 | return schemaTree;
430 | };
431 | Magnicache.prototype.schemaIsValid = function (schema) {
432 | return schema instanceof GraphQLSchema;
433 | };
434 | module.exports = Magnicache;
435 |
--------------------------------------------------------------------------------
/magnicache-server/magnicache-server.ts:
--------------------------------------------------------------------------------
1 | const { GraphQLSchema, graphql } = require('graphql');
2 | const { parse } = require('graphql/language/parser');
3 | import { Response, Request, NextFunction } from 'express';
4 | import { MagnicacheType } from './../types';
5 | // import * as mergeWith from 'lodash.mergewith';
6 | const mergeWith = require('lodash.mergewith');
7 | const { IntrospectionQuery } = require('./IntrospectionQuery');
8 | // TODO?: type "this" more specifically
9 | // TODO?: add query type to linked list => refactor mutations
10 | // TODO?: rename EvictionNode => Node
11 | // TODO?: put Cache.validate on Magnicache prototype, only store schema once
12 |
13 | function Magnicache(this: MagnicacheType, schema: {}, maxSize = 100): void {
14 | if (!this.schemaIsValid(schema)) {
15 | throw new Error(
16 | 'This schema is invalid. Please ensure that the passed in schema is an instance of GraphQLSchema, and that you are using a graphql package of version 14.0.0 or later'
17 | );
18 | }
19 | // save the provided schema
20 | this.schema = schema;
21 | // max size of cache in atomized queries
22 | this.maxSize = maxSize;
23 | // instantiate doublely linked list to handle caching
24 | this.cache = new Cache(maxSize, schema);
25 |
26 | this.schemaTree = this.schemaParser(schema);
27 |
28 | this.metrics = {
29 | cacheUsage: 0,
30 | sizeLeft: this.maxSize,
31 | totalHits: 0,
32 | totalMisses: 0,
33 | AvgCacheTime: 0, // for atomic queries only, can change to query as a whole later on
34 | AvgMissTime: 0,
35 | AvgMemAccTime: 0, // hit rate * cacheTime + miss rate * missTIme, still for atomic queries only
36 | };
37 | // bind method(s)
38 | this.query = this.query.bind(this);
39 | this.schemaParser = this.schemaParser.bind(this);
40 | }
41 | // linked list node constructor
42 | class EvictionNode {
43 | key: string; // atomized query
44 | value: any; // query return value
45 | next: EvictionNode | null;
46 | prev: EvictionNode | null;
47 | constructor(key: string, value: T) {
48 | this.key = key;
49 | this.value = value;
50 | this.next = null;
51 | this.prev = null;
52 | }
53 | }
54 |
55 | // linked list constructor
56 | class Cache {
57 | maxSize: number;
58 | map: Map; // map stores references to linked list nodes
59 | head: EvictionNode | null;
60 | tail: EvictionNode | null;
61 | schema: {};
62 | constructor(maxSize: number, schema: {}) {
63 | this.maxSize = maxSize;
64 | this.map = new Map();
65 | this.head = null;
66 | this.tail = null;
67 | this.schema = schema;
68 |
69 | // method binding
70 | this.create = this.create.bind(this);
71 | this.delete = this.delete.bind(this);
72 | this.get = this.get.bind(this);
73 | this.includes = this.includes.bind(this);
74 | }
75 |
76 | // insert a node at the head of the list && if maxsize is reached, evict least recently used node
77 | create(key: string, value: object): EvictionNode {
78 | // Create a new node
79 | const newNode = new EvictionNode(key, value);
80 |
81 | // add node to map
82 | this.map.set(key, newNode);
83 |
84 | // if linked list is empty, set at head && tail
85 | if (!this.head) {
86 | this.head = newNode;
87 | this.tail = newNode;
88 | // otherwise add new node at head of list
89 | } else {
90 | newNode.next = this.head;
91 | this.head.prev = newNode;
92 | this.head = newNode;
93 | }
94 | // check cache size, prune tail if necessary
95 | if (this.map.size > this.maxSize) this.delete(this.tail);
96 |
97 | return newNode;
98 | }
99 |
100 | // remove node from linked list
101 | // TODO: type node
102 | delete(node: any): void {
103 | // SHOULD NEVER RUN: delete should only be invoked when node is known to exist
104 | if (node === null)
105 | throw new Error('ERROR in MagniCache.cache.delete: node is null');
106 | if (node.next === null && node.prev === null) {
107 | this.head = null;
108 | this.tail = null;
109 | // if node is at tail
110 | } else if (node.next === null) {
111 | node.prev.next = null;
112 | this.tail = node.prev;
113 | // if node is at head
114 | } else if (node.prev === null) {
115 | node.next.prev = null;
116 | this.head = node.next;
117 | // if node is not head of tail;
118 | } else {
119 | node.prev.next = node.next;
120 | node.next.prev = node.prev;
121 | }
122 | // remove node from map
123 | this.map.delete(node.key);
124 | }
125 |
126 | // retrieve node from linked list
127 | get(key: string): string {
128 | const node: EvictionNode = this.map.get(key);
129 | // SHOULD NEVER RUN: get should only be invoked when node is known to exist
130 | if (node === null)
131 | throw new Error('ERROR in MagniCache.cache.get: node is null');
132 | // if the node is at the head, simply return the value
133 | if (node.prev === null) return node.value;
134 | // if node is at the tail, remove it from the tail
135 | else if (node.next === null) {
136 | this.tail = node.prev;
137 | node.prev.next = null;
138 | // if node is neither, remove it
139 | } else {
140 | node.prev.next = node.next;
141 | node.next.prev = node.prev;
142 | }
143 | // move the current head down one in the LL, move the current node to the head
144 | this.head!.prev = node;
145 | node.prev = null;
146 | node.next = this.head;
147 | this.head = node;
148 |
149 | return node.value;
150 | }
151 | // TODO: type node
152 | validate(node: any): void {
153 | if (node?.key === null)
154 | throw new Error('ERROR in MagniCache.cache.validate: invalid node');
155 | graphql({ schema: this.schema, source: node.key })
156 | .then((result: { error?: {}; data?: {} }) => {
157 | if (!result.error) {
158 | node.value = result;
159 | } else {
160 | this.delete(node);
161 | }
162 | })
163 | .catch((err: {}) => {
164 | throw new Error(
165 | 'ERROR in MagniCache.cache.validate: error executing graphql query'
166 | );
167 | });
168 | return;
169 | }
170 |
171 | // syntactic sugar to check if cache has a key
172 | includes(key: string): boolean {
173 | return this.map.has(key);
174 | }
175 | // syntactic sugar to get linked list length
176 | count(): number {
177 | return this.map.size;
178 | }
179 | }
180 |
181 | // used in middleware chain
182 | Magnicache.prototype.query = function (
183 | req: Request,
184 | res: Response,
185 | next: NextFunction
186 | ): void {
187 | // get graphql query from request body
188 |
189 | const { query } = req.body;
190 |
191 | // if query is null, send back a 400 code
192 | // if (query === null || query === '') {
193 | // res.locals.queryResponse = 'missing query body';
194 | // return next();
195 | // }
196 |
197 | //TODO: Make sure to handle the error from parse if it is an invalid query
198 |
199 | // parse the query into an AST
200 | // let ast;
201 | let ast: any;
202 |
203 | try {
204 | const {
205 | definitions: [parsedAst],
206 | } = parse(query);
207 |
208 | ast = parsedAst;
209 | } catch (error) {
210 | res.locals.queryResponse = 'Invalid query';
211 | return next();
212 | console.error('An error occurred while parsing the query:', error);
213 | }
214 |
215 | // if query is for 'clearCache', clear the cache and return next
216 | if (ast.selectionSet.selections[0].name.value === 'clearCache') {
217 | this.cache = new Cache(this.maxSize, this.schema);
218 | res.locals.queryResponse = { cacheStatus: 'cacheCleared' };
219 | return next();
220 | }
221 |
222 | // if query is for metrics, attach metrics to locals and return next
223 | if (ast.selectionSet.selections[0].name.value === 'getMetrics') {
224 | res.locals.queryResponse = this.metrics;
225 | return next();
226 | }
227 |
228 | // check if the operation type is a query
229 | if (ast.operation === 'query') {
230 | // if query is for the schema, bypass cache and execute query as normal
231 | if (ast.selectionSet.selections[0].name.value === '__schema') {
232 | graphql({ schema: this.schema, source: query })
233 | .then((result: {}) => {
234 | res.locals.queryResponse = result;
235 | return next();
236 | })
237 | // throw error to express global error handler
238 | .catch((err: {}) => {
239 | throw new Error(
240 | 'ERROR executing graphql query' + JSON.stringify(err)
241 | );
242 | });
243 | } else {
244 | // parse the ast into usable graphql querys
245 | const queries: string[] = this.magniParser(ast.selectionSet.selections);
246 |
247 | const queryResponses: {}[] = [];
248 | // compile all queryResponses into one object that is return to requester
249 | const compileQueries = () => {
250 | // merge all responses into one object
251 | let response: {} = {};
252 | for (const queryResponse of queryResponses) {
253 | response = mergeWith(response, queryResponse);
254 | }
255 |
256 | // assign the combined result to the response locals
257 | res.locals.queryResponse = response;
258 | return next();
259 | };
260 |
261 | // calculate the average memory access time
262 | const calcAMAT = () => {
263 | // calculate cache hit rate
264 | const hitRate =
265 | this.metrics.totalHits /
266 | (this.metrics.totalHits + this.metrics.totalMisses);
267 |
268 | // calculate average memory access time and update metrics object
269 | this.metrics.AvgMemAccTime = Math.round(
270 | hitRate * this.metrics.AvgCacheTime +
271 | (1 - hitRate) * this.metrics.AvgMissTime
272 | );
273 |
274 | // Return the calculated metric
275 | return this.metrics.AvgMemAccTime;
276 | };
277 |
278 | // loop through the individual queries and execute them in turn
279 | for (const query of queries) {
280 | // check if query is already cached
281 | if (this.cache.includes(query)) {
282 | // add a cookie indicating that the cache was hit
283 | // will be overwritten by any following querys
284 | res.cookie('cacheStatus', 'hit');
285 | // update the metrics with a hit count
286 | this.metrics.totalHits++;
287 | // Start a timter
288 | const hitStart = Date.now();
289 | // retrieve the data from cache and add to queryResponses array
290 | queryResponses.push(this.cache.get(query));
291 |
292 | // check if all queries have been fetched
293 | if (queries.length === queryResponses.length) {
294 | // compile all queries
295 | compileQueries();
296 | }
297 |
298 | // calculate the hit time
299 | const hitTime = Math.floor(Date.now() - hitStart);
300 | // update the metrics object
301 | this.metrics.AvgCacheTime = Math.round(
302 | (this.metrics.AvgCacheTime + hitTime) / this.metrics.totalHits
303 | );
304 | } else {
305 | // start the miss timer
306 | const missStart = Date.now();
307 | // execute the query
308 | graphql({ schema: this.schema, source: query })
309 | .then((result: { err?: {}; data?: {} }) => {
310 | // if no error, cache response
311 | if (!result.err) this.cache.create(query, result);
312 |
313 | // update the metrics with the new size
314 | this.metrics.cacheUsage = this.cache.count();
315 |
316 | // store the query response
317 | queryResponses.push(result);
318 | })
319 | // update miss time as well as update the average memory access time
320 | .then(() => {
321 | // add a cookie indicating that the cache was missed
322 | res.cookie('cacheStatus', 'miss');
323 | // update the metrics with a missCount
324 | this.metrics.totalMisses++;
325 | this.sizeLeft = this.maxSize - this.metrics.cacheUsage;
326 | const missTime = Date.now() - missStart;
327 | this.metrics.AvgMissTime = Math.round(
328 | (this.metrics.AvgMissTime + missTime) / this.metrics.totalMisses
329 | );
330 | this.metrics.AvgMissTime == Math.round(this.metrics.AvgMissTime);
331 | calcAMAT();
332 | // check if all queries have been fetched
333 | if (queries.length === queryResponses.length) {
334 | // compile all queries
335 | compileQueries();
336 | }
337 | })
338 | .catch((err: {}) => {
339 | throw new Error(
340 | 'ERROR executing graphql query' + JSON.stringify(err)
341 | );
342 | });
343 | }
344 | }
345 | }
346 | // if not a query
347 | } else if (ast.operation === 'mutation') {
348 | // first execute mutation normally
349 | graphql({ schema: this.schema, source: query })
350 | .then((result: {}) => {
351 | res.locals.queryResponse = result;
352 | return next();
353 | })
354 | .then(() => {
355 | // get all mutation types, utilizing a set to avoid duplicates
356 | const mutationTypes: Set = new Set();
357 | for (const mutation of ast.selectionSet.selections) {
358 | const mutationName = mutation.name.value;
359 | mutationTypes.add(this.schemaTree.mutations[mutationName]);
360 | }
361 | // for every mutation type, get every corresponding query type
362 | mutationTypes.forEach((mutationType) => {
363 | const userQueries: Set = new Set();
364 |
365 | for (const query in this.schemaTree.queries) {
366 | const type = this.schemaTree.queries[query];
367 | if (mutationType === type) userQueries.add(query);
368 | }
369 |
370 | userQueries.forEach((query) => {
371 | for (
372 | let currentNode = this.cache.head;
373 | currentNode !== null;
374 | currentNode = currentNode.next
375 | ) {
376 | if (currentNode.key.includes(query)) {
377 | this.cache.validate(currentNode);
378 | }
379 | }
380 | });
381 | });
382 | })
383 | // TODO: type err
384 | .catch((err: any) => {
385 | console.error(err);
386 | return err;
387 | });
388 | }
389 | };
390 |
391 | // invoked with AST as an argument, returns an array of graphql schemas
392 | Magnicache.prototype.magniParser = function (
393 | selections: {
394 | kind: string;
395 | name: {
396 | kind: string;
397 | value: string;
398 | };
399 | arguments: {
400 | kind: string;
401 | name: {
402 | kind: string;
403 | value: string;
404 | };
405 | value: {
406 | kind: string;
407 | value: string;
408 | };
409 | }[];
410 | selectionSet?: {
411 | kind: string;
412 | selections: typeof selections;
413 | };
414 | }[],
415 | queryArray: (string | string[])[] = [],
416 | queries: string[] = []
417 | ): string[] {
418 | // Looping through the selections to build the queries array
419 | for (const selection of selections) {
420 | // add current query to queryArray
421 | queryArray.push(selection.name.value);
422 |
423 | // if a query has arguments, format and add to query array
424 | if (selection.arguments.length > 0) {
425 | const argumentArray: string[] = [];
426 |
427 | // looping through the arguments to add them to the argument array
428 | for (const argument of selection.arguments) {
429 | argumentArray.push(`${argument.name.value}:${argument.value.value}`);
430 | }
431 |
432 | queryArray.push([argumentArray.join(',')]);
433 | }
434 | // if there is a selectionSet property, there are more deeply nested queries
435 | if (selection.selectionSet) {
436 | // recursively invoke magniParser passing in selections array
437 | this.magniParser(selection.selectionSet.selections, queryArray, queries);
438 | } else {
439 | let queryString = ``;
440 | // if query array looks like this: ['messageById', ['id:4'], 'message']
441 | // formated query will look like this: {allMessages(id:4){message}}
442 |
443 | // looping through the query array in reverse to build the query string
444 | for (let i = queryArray.length - 1; i >= 0; i--) {
445 | // arguments are put into an array and need to be formatted differently
446 | if (Array.isArray(queryArray[i])) {
447 | queryString = `(${queryArray[i][0]})${queryString}`;
448 | } else {
449 | queryString = `{${queryArray[i] + queryString}}`;
450 | }
451 | }
452 | // adding the completed query to the queries array
453 | queries.push(queryString);
454 | }
455 | // remove last element, as it's not wanted on next iteration
456 | queryArray.pop();
457 | }
458 | // returning the array of individual querys
459 | return queries;
460 | };
461 |
462 | Magnicache.prototype.schemaParser = function (schema: typeof this.schema) {
463 | // TODO: refactor to be able to store multiple types for each query
464 | // TODO: stricter types for schemaTree
465 | const schemaTree: { queries: any; mutations: any } = {
466 | queries: {
467 | //Ex: allMessages:Messages
468 | },
469 | mutations: {},
470 | };
471 | // TODO: type 'type'
472 | // TODO: refactor to ensure there isn't an infinite loop
473 | const typeFinder = (type: any): string => {
474 | // console.log('field', type);
475 | if (type.name === null) return typeFinder(type.ofType);
476 | return type.name;
477 | };
478 |
479 | // TODO: Type the result for the schema
480 | graphql({ schema: this.schema, source: IntrospectionQuery })
481 | .then((result: any) => {
482 | // console.log(result.data.__schema.queryType);
483 | if (result.data.__schema.queryType) {
484 | for (const field of result.data.__schema.queryType.fields) {
485 | schemaTree.queries[field.name] = typeFinder(field.type);
486 | }
487 | }
488 | if (result.data.__schema.mutationType) {
489 | for (const field of result.data.__schema.mutationType.fields) {
490 | schemaTree.mutations[field.name] = typeFinder(field.type);
491 | }
492 | }
493 | })
494 | .then(() => {
495 | // console.log('schemaTree', schemaTree);
496 | })
497 | // throw error to express global error handler
498 | .catch((err: {}) => {
499 | console.error(err);
500 | // throw new Error(`ERROR executing graphql query` + JSON.stringify(err));
501 | return err;
502 | });
503 |
504 | return schemaTree;
505 | };
506 |
507 | Magnicache.prototype.schemaIsValid = function (schema) {
508 | return schema instanceof GraphQLSchema;
509 | };
510 |
511 | module.exports = Magnicache;
512 |
--------------------------------------------------------------------------------
/magnicache-server/mutationAST.json:
--------------------------------------------------------------------------------
1 | {
2 | "kind": "Document",
3 | "definitions": [
4 | {
5 | "kind": "OperationDefinition",
6 | "operation": "mutation",
7 | "variableDefinitions": [],
8 | "directives": [],
9 | "selectionSet": {
10 | "kind": "SelectionSet",
11 | "selections": [
12 | {
13 | "kind": "Field",
14 | "name": {
15 | "kind": "Name",
16 | "value": "addMessage"
17 | },
18 | "arguments": [
19 | {
20 | "kind": "Argument",
21 | "name": {
22 | "kind": "Name",
23 | "value": "sender_id"
24 | },
25 | "value": {
26 | "kind": "IntValue",
27 | "value": "12"
28 | }
29 | },
30 | {
31 | "kind": "Argument",
32 | "name": {
33 | "kind": "Name",
34 | "value": "message"
35 | },
36 | "value": {
37 | "kind": "StringValue",
38 | "value": "This is a new message part1 ",
39 | "block": false
40 | }
41 | }
42 | ],
43 | "directives": [],
44 | "selectionSet": {
45 | "kind": "SelectionSet",
46 | "selections": [
47 | {
48 | "kind": "Field",
49 | "name": {
50 | "kind": "Name",
51 | "value": "message"
52 | },
53 | "arguments": [],
54 | "directives": []
55 | },
56 | {
57 | "kind": "Field",
58 | "name": {
59 | "kind": "Name",
60 | "value": "sender_id"
61 | },
62 | "arguments": [],
63 | "directives": []
64 | }
65 | ]
66 | }
67 | }
68 | ]
69 | }
70 | }
71 | ]
72 | }
73 |
--------------------------------------------------------------------------------
/magnicache-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@magnicache/server",
3 | "version": "1.0.9",
4 | "description": "Server side caching for GraphQL",
5 | "main": "magnicache-server.js",
6 | "scripts": {
7 | "test-server": "jest --rootDir=../__tests__ magnicache-server.test.ts",
8 | "test-client": "jest --rootDir=../__tests__ magnicache-client.test.ts"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/oslabs-beta/MagniCache.git"
13 | },
14 | "dependencies": {
15 | "graphql": ">= 14.0.0",
16 | "lodash.mergewith": "^4.6.2"
17 | },
18 | "devDependencies": {
19 | "@types/jest": "^29.4.0",
20 | "jest": "^29.5.0",
21 | "pg": "^8.10.0",
22 | "ts-jest": "^29.0.5"
23 | },
24 | "author": "MagniCache",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/oslabs-beta/MagniCache/issues"
28 | },
29 | "homepage": "https://github.com/oslabs-beta/MagniCache#readme",
30 | "keywords": [
31 | "cache",
32 | "caching",
33 | "graphQL",
34 | "server",
35 | "serverside",
36 | "server-side",
37 | "express",
38 | "middleware",
39 | "express-middleware",
40 | "lightweight",
41 | "localstorage",
42 | "magnicache",
43 | "performance",
44 | "API",
45 | "query",
46 | "in-memory",
47 | "endpoint",
48 | "speed"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/magnicache-server/schema.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLObjectType,
3 | GraphQLString,
4 | GraphQLInt,
5 | GraphQLList,
6 | GraphQLSchema,
7 | } = require('graphql');
8 |
9 | const db = require('./db');
10 |
11 | // Define the Customer type
12 | const CustomerType = new GraphQLObjectType({
13 | name: 'Customer',
14 | fields: () => ({
15 | id: { type: GraphQLInt },
16 | name: { type: GraphQLString },
17 | address: { type: GraphQLString },
18 | zip: { type: GraphQLString },
19 | sales: {
20 | type: new GraphQLList(SalesType),
21 | resolve(parent, args) {
22 | // Query the sales table for sales by this customer
23 | // and return them as an array
24 | return db
25 | .query(`SELECT * FROM sales WHERE customer_id = ${parent.id}`)
26 | .then((res) => res.rows)
27 | .catch((err) => console.error(err));
28 | },
29 | },
30 | }),
31 | });
32 |
33 | // Define the Sales type
34 | const SalesType = new GraphQLObjectType({
35 | name: 'Sales',
36 | fields: () => ({
37 | date: { type: GraphQLString },
38 | total: { type: GraphQLInt },
39 | customer: {
40 | type: CustomerType,
41 | resolve(parent, args) {
42 | // Query the customer table for the customer associated
43 | // with this sale and return it
44 | return db
45 | .query(`SELECT * FROM customers WHERE id = ${parent.customer_id}`)
46 | .then((res) => res.rows[0])
47 | .catch((err) => console.error(err));
48 | },
49 | },
50 | items: {
51 | type: new GraphQLList(SalesItemType),
52 | resolve(parent, args) {
53 | // Query the sales_item table for items associated
54 | // with this sale and return them as an array
55 | return db
56 | .query(`SELECT * FROM sales_item WHERE sale_id = ${parent.id}`)
57 | .then((res) => res.rows)
58 | .catch((err) => console.error(err));
59 | },
60 | },
61 | }),
62 | });
63 |
64 | // Define the Sales Item type
65 | const SalesItemType = new GraphQLObjectType({
66 | name: 'SalesItem',
67 | fields: () => ({
68 | quantity: { type: GraphQLInt },
69 | product: {
70 | type: ProductType,
71 | resolve(parent, args) {
72 | // Query the products table for the product associated
73 | // with this sales item and return it
74 | return db
75 | .query(`SELECT * FROM products WHERE id = ${parent.product_id}`)
76 | .then((res) => res.rows[0])
77 | .catch((err) => console.error(err));
78 | },
79 | },
80 | }),
81 | });
82 |
83 | // Define the Product type
84 | const ProductType = new GraphQLObjectType({
85 | name: 'Product',
86 | fields: () => ({
87 | id: { type: GraphQLInt },
88 | name: { type: GraphQLString },
89 | sales_items: {
90 | type: new GraphQLList(SalesItemType),
91 | resolve(parent, args) {
92 | // Query the sales_item table for sales items associated
93 | // with this product and return them as an array
94 | return db
95 | .query(`SELECT * FROM sales_item WHERE product_id = ${parent.id}`)
96 | .then((res) => res.rows)
97 | .catch((err) => console.error(err));
98 | },
99 | },
100 | }),
101 | });
102 |
103 | // Define the root Query type
104 | const RootQuery = new GraphQLObjectType({
105 | name: 'RootQueryType',
106 | fields: {
107 | customer: {
108 | type: CustomerType,
109 | args: { id: { type: GraphQLInt } },
110 | resolve(parent, args) {
111 | // Query the customer table for a customer with the specified ID
112 | // and return it
113 | return db
114 | .query(`SELECT * FROM customers WHERE id = ${args.id}`)
115 | .then((res) => res.rows[0])
116 | .catch((err) => console.error(err));
117 | },
118 | },
119 | customers: {
120 | type: new GraphQLList(CustomerType),
121 | resolve(parent, args) {
122 | // Query the customer table for a customer with the specified ID
123 | // and return it
124 | return db
125 | .query(`SELECT * FROM customers`)
126 | .then((res) => res.rows)
127 | .catch((err) => console.error(err));
128 | },
129 | },
130 | // sales: {},
131 | // products: {},
132 | },
133 | });
134 |
135 | // Define the root Query type
136 | const RootMutation = new GraphQLObjectType({
137 | name: 'RootMutationType',
138 | fields: {},
139 | });
140 |
141 | module.exports = new GraphQLSchema({
142 | query: RootQuery,
143 | });
144 |
--------------------------------------------------------------------------------
/magnicache-server/test.js:
--------------------------------------------------------------------------------
1 | const obj = {
2 | name: 'John',
3 | sayName: () => {
4 | console.log(this.name);
5 | return this.name;
6 | },
7 | invoke: function () {
8 | console.log(this);
9 | console.log(this.sayName).sayName();
10 | },
11 | };
12 |
13 | console.log(obj.invoke()); // undefined
14 |
15 | const queryResponses = [
16 | {data: {messageById: [{ message:
17 | "\n\nHey there everyone! I've been having such a great time getting to know all of you",},],
18 | },
19 | },
20 | { data: { messageById: [{ sender_id: 1 }] } },
21 | ];
22 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command="npm install && cd client && npm install && cd ../magnicache-client && npm install cd ../magnicache-demo && npm install && cd ../magnicache-server && npm install && cd .. && npm run build"
3 | publish="build/"
4 | [dev]
5 | command="npm run dev"
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier": {
3 | "trailingComma": "es5",
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true
7 | },
8 | "name": "magnicache",
9 | "description": "Lightweight caching layer for graphQL",
10 | "version": "0.0.1",
11 | "main": "index.js",
12 | "scripts": {
13 | "test": "echo \"Error: no test specified\" && exit 1",
14 | "build": "NODE_ENV=production webpack",
15 | "dev": "cross-env NODE_ENV=development webpack serve & nodemon ./magnicache-demo/server.js",
16 | "test-server": "jest magnicache-server.test.ts",
17 | "test-client": "jest magnicache-client.test.ts",
18 | "test-demo": "NODE_ENV=test jest magnicache-demo.test.ts"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/oslabs-beta/MagniCache.git"
23 | },
24 | "keywords": [
25 | "cache",
26 | "graphQL"
27 | ],
28 | "author": "MagniCache",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/oslabs-beta/MagniCache/issues"
32 | },
33 | "homepage": "https://github.com/oslabs-beta/MagniCache#readme",
34 | "devDependencies": {
35 | "@babel/core": "^7.20.12",
36 | "@babel/preset-env": "^7.20.2",
37 | "@babel/preset-react": "^7.18.6",
38 | "@svgr/webpack": "^6.5.1",
39 | "@types/jest": "^29.4.0",
40 | "@types/lodash.mergewith": "^4.6.7",
41 | "@types/react": "^18.0.28",
42 | "@types/react-dom": "^18.0.11",
43 | "@types/react-router-dom": "^5.3.3",
44 | "babel-loader": "^9.1.2",
45 | "bootstrap": "^5.2.3",
46 | "concurrently": "^7.6.0",
47 | "cross-env": "^5.2.0",
48 | "css-loader": "^6.7.3",
49 | "file-loader": "^6.2.0",
50 | "graphql": "^16.6.0",
51 | "html-webpack-plugin": "^5.5.0",
52 | "jest": "^29.5.0",
53 | "lodash.mergewith": "^4.6.2",
54 | "nodemon": "^2.0.20",
55 | "pg": "^8.10.0",
56 | "react-bootstrap": "^2.7.2",
57 | "react-router": "^6.8.1",
58 | "react-router-dom": "^6.8.1",
59 | "sass": "^1.57.1",
60 | "sass-loader": "^13.2.0",
61 | "style-loader": "^3.3.1",
62 | "supertest": "^6.3.3",
63 | "throttle": "^1.0.3",
64 | "ts-jest": "^29.0.5",
65 | "ts-loader": "^9.4.2",
66 | "webpack": "^5.75.0",
67 | "webpack-cli": "^5.0.1",
68 | "webpack-dev-server": "^4.11.1"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "esModuleInterop": true,
5 | "removeComments": true,
6 | "preserveConstEnums": true,
7 | "sourceMap": true,
8 | "jsx": "react",
9 | "strict": true,
10 | "noEmitOnError": true,
11 | "types": ["react/next", "node", "jest"],
12 | "target": "ESNext",
13 | "noImplicitAny": true
14 | },
15 | "include": ["client", ".d.ts"],
16 | "exclude": ["node_modules"]
17 | }
18 |
--------------------------------------------------------------------------------
/types.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | exports.__esModule = true;
3 | /*
4 | this.metrics = { //type this to be a cacheMetrics type
5 | cacheUsage: 0,
6 | sizeLeft: this.maxSize,
7 | totalHits: 0,
8 | totalMisses: 0,
9 | AvgCacheTime: 0, //for atomic queries only, can change to query as a whole later on
10 | AvgMissTime: 0,
11 | AvgMemAccTime: 0, // hit rate * cacheTime + miss rate * missTIme
12 | };
13 | */
14 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { Response, Request, NextFunction } from 'express';
2 |
3 | export type Metrics = {
4 | // queryMetrics
5 | fetchTime: number;
6 | cacheStatus: 'hit' | 'miss';
7 | };
8 |
9 | export type CacheMetricsType = {
10 | cacheUsage: number;
11 | sizeLeft: number;
12 | totalHits: number;
13 | totalMisses: number;
14 | AvgCacheTime: number;
15 | AvgMissTime: number;
16 | AvgMemAccTime: number;
17 | };
18 |
19 | export type MagnicacheType = {
20 | schema: {};
21 | maxSize: number;
22 | query: (req: Request, res: Response, next: NextFunction) => void;
23 | cache: {};
24 | metrics: CacheMetricsType;
25 | schemaTree: {
26 | mutations: {};
27 | queries: {
28 | //name:type
29 | //messageById:Message
30 | //schemaTree.queries[messageById] -> Message
31 | };
32 | };
33 | schemaParser: (
34 | schema: MagnicacheType['schema']
35 | ) => MagnicacheType['schemaTree'];
36 | schemaIsValid: (schema: MagnicacheType['schema']) => boolean;
37 | };
38 |
39 | /*
40 | this.metrics = { //type this to be a cacheMetrics type
41 | cacheUsage: 0,
42 | sizeLeft: this.maxSize,
43 | totalHits: 0,
44 | totalMisses: 0,
45 | AvgCacheTime: 0, //for atomic queries only, can change to query as a whole later on
46 | AvgMissTime: 0,
47 | AvgMemAccTime: 0, // hit rate * cacheTime + miss rate * missTIme
48 | };
49 | */
50 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './client/index.tsx',
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'bundle.js',
9 | },
10 | mode: process.env.NODE_ENV,
11 | plugins: [
12 | new HtmlWebpackPlugin({
13 | template: './client/index.html',
14 | }),
15 | ],
16 | devServer: {
17 | // port: 8080,
18 | historyApiFallback: true,
19 | static: {
20 | publicPath: '/',
21 | directory: path.resolve(__dirname, 'build'),
22 | },
23 | proxy: {
24 | '/**': {
25 | target: 'http://localhost:3000/',
26 | secure: false,
27 | },
28 | },
29 | headers: {
30 | 'Access-Control-Allow-Origin': '*',
31 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
32 | 'Access-Control-Allow-Headers':
33 | 'X-Requested-With, content-type, Authorization',
34 | },
35 | },
36 | module: {
37 | rules: [
38 | {
39 | test: /\.jsx?/,
40 | exclude: /(node_modules)/,
41 | use: {
42 | loader: 'babel-loader',
43 | options: {
44 | presets: ['@babel/preset-env', '@babel/preset-react'],
45 | },
46 | },
47 | },
48 | {
49 | test: /\.tsx?$/,
50 | exclude: /(node_modules)/,
51 | use: 'ts-loader',
52 | },
53 | {
54 | test: /\.(saas|less|css|scss)$/,
55 | use: ['style-loader', 'css-loader', 'sass-loader'],
56 | },
57 | {
58 | test: /\.svg$/i,
59 | issuer: /\.[jt]sx?$/,
60 | use: ['@svgr/webpack'],
61 | },
62 | {
63 | test: /\.(png|jpe?g|gif)$/i,
64 | use: [
65 | {
66 | loader: 'file-loader',
67 | },
68 | ],
69 | },
70 | ],
71 | },
72 | resolve: {
73 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
74 | },
75 | externals: {
76 | pg: 'commonjs pg',
77 | },
78 | };
79 |
--------------------------------------------------------------------------------