├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.ts
├── package.json
├── src
├── CacheWhole.ts
├── ResolverCache.ts
├── SchemaWithCache.ts
├── SplacheCache.ts
├── __test__
│ ├── CacheWhole.test.ts
│ ├── RedisConnection.test.ts
│ ├── Resolver.test.ts
│ ├── SplacheCache.test.ts
│ ├── SplacheCacheHelpers.test.ts
│ └── docker-compose-test.yml
├── index-test.ts
├── schemaTest.ts
├── starWarsData.ts
└── starWarsDataResolver.ts
└── ts.config.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:@typescript-eslint/recommended"
11 | ],
12 | "overrides": [
13 | ],
14 | "parser": "@typescript-eslint/parser",
15 | "parserOptions": {
16 | "ecmaVersion": "latest"
17 | },
18 | "plugins": [
19 | "react",
20 | "@typescript-eslint"
21 | ],
22 | "rules": {
23 | "no-var-requires": "off"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #ignore node_modules
2 | node_modules
3 | #ignore json packages
4 | package-lock.json
5 | #ignore output file 'dist' of complier
6 | dist
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |

5 |
A Lightweight NPM package for GraphQL Caching with Redis
6 |
7 |
8 | 
9 | 
10 | [](public/LICENSE)
11 |
12 |
13 | Introducing Splache (/splæʃ/), an agile, user-friendly, and lightweight JavaScript library that efficiently caches GraphQL queries using the power of Redis. The Splache library is designed for improved performance and reduced load on your GraphQL server. Through leveraging the speed and scalability of Redis, Splache is able to provide an efficient and unique solution for caching GraphQL queries!
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | [Launch Page](www.splachejs.com) : Learn More about Splache & Demo our package via an interactive sandbox pre-installation
23 |
24 | [Install our NPM Package](https://www.npmjs.com/package/splache)
25 |
26 | [Documentation](https://medium.com/@zhangn356/exploring-caching-solutions-for-graphql-an-introduction-to-splache-4a497bdb597f)
27 |
28 |
29 |
30 | ## Get Started and Installation
31 | Run ```npm install splache``` to install the package to import ```SplacheCacheWhole```, ```ResolverCache``` and ```SplacheCache```.
32 |
33 | ### Quick Start with Redis
34 | If you are new to Redis, here is the [documentation](https://redis.io/docs/getting-started/) to install redis and start a redis instance locally.
35 |
36 | ### Import splache
37 |
38 | ```node
39 | import splache from 'splache'
40 |
41 | ```
42 |
43 |
44 |
45 | ## Key Features
46 |
47 | 1. The Caching of Whole Queries
48 |
49 |
50 | Simply provide SpacheCacheWhole your schema, redis host, redis port, and password (Only provide the password if your external redis instance is password protected. If not, omit the password. Additionally, omit host, port, and password arguments if connecting to local redis instance) and then direct your queries through the middleware as seen in the example below.
51 |
52 |
53 | ```node
54 | import { SplacheCacheWhole }
55 |
56 | const cache = new SplacheCacheWhole(schema, host, post, password);
57 |
58 | app.use('/graphql', cache.wholeCache, (req, res) => {
59 | res.send(res.locals.queryResponse)
60 | })
61 | ```
62 |
63 |
64 | 2. The Caching of Resolvers
65 |
66 | ```node
67 | import { ResolverCache } from 'Splache'
68 |
69 | const cache = new ResolverCache(host, port, password)
70 | ```
71 |
72 | Upon importing ResolverCache from our package, create a new instance of ResolverCache to access the ‘checkCache’ method. From there, simply wrap your resolver functions with our pre-built functionality.
73 |
74 | Here is an example:
75 |
76 |
77 | ```node
78 | planet : {
79 | type: Planet,
80 | args: {
81 | id : {
82 | description: 'id of the planet'
83 | type: new GraphQLNonNull(GraphQLString)
84 | }
85 | },
86 | resolve: ((parent, args, context, info) => cache.checkCache(parents, args, context, info, getPlanet))
87 | }
88 | ```
89 |
90 |
91 | 3. The Caching of Normalized Query Strings & Breakdown of Complex Nested Queries
92 |
93 | Create a new instance of SplacheCache, passing in your schema, host, port, and password (omit host, port, and password if just connecting to local redis instance). By passing your query through our GQLquery method, it’ll generalize and split your query string and check the cache for these individual split queries. This reduces redundancy in your cache if existing cached queries are nested into a complex nested query.
94 |
95 |
96 | ```node
97 | import { SplacheCache } from 'splache'
98 | const cache = newSplacheCache(schema, host, port, password);
99 |
100 | app.use('/graphql', cacheGQLquery, (req, res) => {
101 | res.send(res.locals.queryResponse)
102 | })
103 | ```
104 |
105 | ## Currently Under Development
106 |
107 |
108 | ## Connect with the Team!
109 | | Nicholas Cathcart | Nicolas Jackson | Jessica Wang | Nancy Zhang |
110 | | :---: | :---: | :---: | :---: |
111 | | [](https://github.com/nhcathcart) [](https://www.linkedin.com/in/nicholas-cathcart-4b3834267/)| [](https://github.com/NicJax) [](www.linkedin.com/in/NicJax) | [](https://github.com/jesswang-dev) [](https://www.linkedin.com/in/jessica-xuecen-wang) | [](https://github.com/zhangn356 ) [](https://www.linkedin.com/in/zhangn356) |
112 |
113 | ## Want to Contribute?
114 |
115 | Splache is an open-source product that is open to input and contributions from the community. After trying out the product, feel free to raise issues or submit a PR request.
116 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/preset-env', {targets: {node: 'current'}}],
3 | '@babel/preset-typescript'
4 | ],
5 | };
6 |
7 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@jest/types"
2 |
3 | const config: Config.InitialOptions = {
4 | preset: "ts-jest",
5 | testEnvironment: "node",
6 | verbose: true,
7 | automock: false,
8 | }
9 | export default config;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "splache",
3 | "version": "1.0.0",
4 | "description": "A lightweight caching solution for GraphQL using Redis",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "build": "tsc --noEmit false",
9 | "lint": "eslint ./src --fix"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/oslabs-beta/CoolQL.git"
14 | },
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/oslabs-beta/CoolQL/issues"
19 | },
20 | "homepage": "https://github.com/oslabs-beta/CoolQL#readme",
21 | "devDependencies": {
22 | "@babel/preset-typescript": "^7.18.6",
23 | "@types/express": "^4.17.17",
24 | "@types/jest": "^29.4.0",
25 | "@typescript-eslint/eslint-plugin": "^5.50.0",
26 | "@typescript-eslint/parser": "^5.50.0",
27 | "child_process": "^1.0.2",
28 | "eslint": "^8.33.0",
29 | "eslint-plugin-react": "^7.32.2",
30 | "jest": "^29.4.3",
31 | "redis-mock": "^0.56.3",
32 | "ts-jest": "^29.0.5",
33 | "typescript": "^4.9.5",
34 | "ts-node": "^10.9.1",
35 | "express": "^4.18.2"
36 | },
37 | "dependencies": {
38 | "graphql": "^16.6.0",
39 | "redis": "^4.6.4"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/CacheWhole.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from 'redis';
2 | import { graphql, GraphQLSchema } from 'graphql';
3 | import { RedisClientType } from '@redis/client';
4 | import { Request, Response, NextFunction } from 'express';
5 |
6 | //creates an importable SplacheCacheWhole Class that accepts a graphQL schema and connects to the user's local Redis client or provided Redis client
7 | export class SplacheCacheWhole {
8 |
9 | schema: GraphQLSchema
10 | client: RedisClientType
11 |
12 | constructor(schema: GraphQLSchema, host?: string, port?: number, password?: string ){
13 |
14 | this.schema = schema;
15 | if(host && port && password){
16 | this.client = createClient({
17 | socket: {
18 | host, port
19 | },
20 | password
21 | })
22 | } else if(host && port){
23 | this.client = createClient({
24 | socket: {
25 | host, port
26 | }})
27 | } else{
28 | this.client = createClient()
29 | }
30 | this.wholeCache = this.wholeCache.bind(this)
31 | this.client.connect()
32 | .then(() => console.log('connected to redis instance'))
33 | .catch((err) => console.log(`there was a problem connecting to the redis instance: ${err}`));
34 | }
35 |
36 | //The method wholeCache is an express middleware function, it examines if queries coming from the request body already exists in the cache
37 | async wholeCache (req: Request, res: Response, next: NextFunction) {
38 | const queryString : string = req.body.query;
39 | const isInCache = await this.client.EXISTS(queryString);
40 | if (isInCache){
41 | const returnObj = await this.client.GET(queryString);
42 | if (typeof returnObj === 'string'){
43 | const returnObjParsed = JSON.parse(returnObj);
44 | res.locals.queryResult = returnObjParsed;
45 | return next();
46 | }
47 | else {
48 | return next({err: 'There was a redis error'})
49 | }
50 | } else {
51 | graphql({ schema: this.schema, source: queryString})
52 | .then((response) => {
53 | this.client.SET(queryString, JSON.stringify(response))
54 | res.locals.queryResult = response;
55 | return next();
56 | })
57 | .catch((err) => next({err}));
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/ResolverCache.ts:
--------------------------------------------------------------------------------
1 | import {createClient} from 'redis'
2 | import { RedisClientType } from '@redis/client';
3 |
4 |
5 | //creates an importable ResolverCache Class that connects to the user's local Redis client or provided Redis client
6 | export class ResolverCache {
7 | client: RedisClientType
8 | constructor(host?:string, port?: number, password?: string){
9 | if(host && port && password){
10 | this.client = createClient({
11 | socket: {
12 | host,port
13 | },
14 | password
15 | })
16 | }else if(host && port){
17 | this.client = createClient({
18 | socket: {
19 | host,port
20 | }})
21 | }else{
22 | this.client = createClient()
23 | }
24 | this.checkCache = this.checkCache.bind(this)
25 | this.client.connect()
26 | .then(() => console.log('connected to redis instance'))
27 | .catch((err) => console.log(`there was a problem connecting to the redis instance: ${err}`))
28 |
29 | }
30 |
31 | //all instances of ResolverCache have access to the checkCache method which checks the user's cache
32 | //if the key already exists in the cache, the result is returned from the user's cache
33 | //if not, it is added to the cache with the corresponding result
34 | async checkCache (parent: any, args: any, context: any, info: {returnType: any}, callback:any){
35 | const key = makeKey(info, args)
36 | const isInCache = await this.client.EXISTS(key)
37 | if (isInCache){
38 | const returnObj = await this.client.GET(key);
39 | if (typeof returnObj === 'string'){
40 | const returnObjParsed = JSON.parse(returnObj);
41 | return returnObjParsed
42 | }
43 | }else{
44 | const returnObj = callback(args)
45 | await this.client.SET(key, JSON.stringify(returnObj));
46 | return returnObj
47 | }
48 | }
49 |
50 | //used with mutations that need to update existing information in cache
51 | async updateCache (parent: any, args: any, context, info: {returnType: any}, callback:any) {
52 | const key = makeKey(info, args)
53 | const returnObj = callback(args)
54 | await this.client.SET(key, JSON.stringify(returnObj));
55 | return returnObj
56 | }
57 | }
58 |
59 | //creates a key that will be the fieldName concatenated with the argument id
60 | export function makeKey (info:any, args:any){
61 | let key = '';
62 | if (Object.keys(args).length === 0) key = info.returnType;
63 | else key = String(info.returnType) + String(args.id);
64 | return key
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/src/SchemaWithCache.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLEnumType,
3 | GraphQLInterfaceType,
4 | GraphQLList,
5 | GraphQLNonNull,
6 | GraphQLObjectType,
7 | } from 'graphql';
8 | import { GraphQLString } from 'graphql';
9 | import { GraphQLSchema } from 'graphql';
10 |
11 | import { getDroid, getFriends, getHero, getHuman, updateHuman } from './starWarsDataResolver';
12 |
13 | import { ResolverCache } from './ResolverCache';
14 |
15 | const resolverCache = new ResolverCache();
16 | /**
17 | * This is designed to be an end-to-end test, demonstrating
18 | * the full GraphQL stack.
19 | *
20 | * We will create a GraphQL schema that describes the major
21 | * characters in the original Star Wars trilogy.
22 | *
23 | * NOTE: This may contain spoilers for the original Star
24 | * Wars trilogy.
25 | */
26 |
27 | /**
28 | * Using our shorthand to describe type systems, the type system for our
29 | * Star Wars example is:
30 | *
31 | * ```graphql
32 | * enum Episode { NEW_HOPE, EMPIRE, JEDI }
33 | *
34 | * interface Character {
35 | * id: String!
36 | * name: String
37 | * friends: [Character]
38 | * appearsIn: [Episode]
39 | * }
40 | *
41 | * type Human implements Character {
42 | * id: String!
43 | * name: String
44 | * friends: [Character]
45 | * appearsIn: [Episode]
46 | * homePlanet: String
47 | * }
48 | *
49 | * type Droid implements Character {
50 | * id: String!
51 | * name: String
52 | * friends: [Character]
53 | * appearsIn: [Episode]
54 | * primaryFunction: String
55 | * }
56 | *
57 | * type Query {
58 | * hero(episode: Episode): Character
59 | * human(id: String!): Human
60 | * droid(id: String!): Droid
61 | * }
62 | * ```
63 | *
64 | * We begin by setting up our schema.
65 | */
66 |
67 | /**
68 | * The original trilogy consists of three movies.
69 | *
70 | * This implements the following type system shorthand:
71 | * ```graphql
72 | * enum Episode { NEW_HOPE, EMPIRE, JEDI }
73 | * ```
74 | */
75 | const episodeEnum = new GraphQLEnumType({
76 | name: 'Episode',
77 | description: 'One of the films in the Star Wars Trilogy',
78 | values: {
79 | NEW_HOPE: {
80 | value: 4,
81 | description: 'Released in 1977.',
82 | },
83 | EMPIRE: {
84 | value: 5,
85 | description: 'Released in 1980.',
86 | },
87 | JEDI: {
88 | value: 6,
89 | description: 'Released in 1983.',
90 | },
91 | },
92 | });
93 |
94 | /**
95 | * Characters in the Star Wars trilogy are either humans or droids.
96 | *
97 | * This implements the following type system shorthand:
98 | * ```graphql
99 | * interface Character {
100 | * id: String!
101 | * name: String
102 | * friends: [Character]
103 | * appearsIn: [Episode]
104 | * secretBackstory: String
105 | * }
106 | * ```
107 | */
108 | const characterInterface: GraphQLInterfaceType = new GraphQLInterfaceType({
109 | name: 'Character',
110 | description: 'A character in the Star Wars Trilogy',
111 | fields: () => ({
112 | id: {
113 | type: new GraphQLNonNull(GraphQLString),
114 | description: 'The id of the character.',
115 | },
116 | name: {
117 | type: GraphQLString,
118 | description: 'The name of the character.',
119 | },
120 | friends: {
121 | type: new GraphQLList(characterInterface),
122 | description:
123 | 'The friends of the character, or an empty list if they have none.',
124 | },
125 | appearsIn: {
126 | type: new GraphQLList(episodeEnum),
127 | description: 'Which movies they appear in.',
128 | },
129 | secretBackstory: {
130 | type: GraphQLString,
131 | description: 'All secrets about their past.',
132 | },
133 | }),
134 | resolveType(character) {
135 | switch (character.type) {
136 | case 'Human':
137 | return humanType.name;
138 | case 'Droid':
139 | return droidType.name;
140 | }
141 | },
142 | });
143 |
144 | /**
145 | * We define our human type, which implements the character interface.
146 | *
147 | * This implements the following type system shorthand:
148 | * ```graphql
149 | * type Human : Character {
150 | * id: String!
151 | * name: String
152 | * friends: [Character]
153 | * appearsIn: [Episode]
154 | * secretBackstory: String
155 | * }
156 | * ```
157 | */
158 | const humanType = new GraphQLObjectType({
159 | name: 'Human',
160 | description: 'A humanoid creature in the Star Wars universe.',
161 | fields: () => ({
162 | id: {
163 | type: new GraphQLNonNull(GraphQLString),
164 | description: 'The id of the human.',
165 | },
166 | name: {
167 | type: GraphQLString,
168 | description: 'The name of the human.',
169 | },
170 | friends: {
171 | type: new GraphQLList(characterInterface),
172 | description:
173 | 'The friends of the human, or an empty list if they have none.',
174 | resolve: (human) => getFriends(human),
175 | },
176 | appearsIn: {
177 | type: new GraphQLList(episodeEnum),
178 | description: 'Which movies they appear in.',
179 | },
180 | homePlanet: {
181 | type: GraphQLString,
182 | description: 'The home planet of the human, or null if unknown.',
183 | },
184 | secretBackstory: {
185 | type: GraphQLString,
186 | description: 'Where are they from and how they came to be who they are.',
187 | resolve() {
188 | throw new Error('secretBackstory is secret.');
189 | },
190 | },
191 | }),
192 | interfaces: [characterInterface],
193 | });
194 |
195 | /**
196 | * The other type of character in Star Wars is a droid.
197 | *
198 | * This implements the following type system shorthand:
199 | * ```graphql
200 | * type Droid : Character {
201 | * id: String!
202 | * name: String
203 | * friends: [Character]
204 | * appearsIn: [Episode]
205 | * secretBackstory: String
206 | * primaryFunction: String
207 | * }
208 | * ```
209 | */
210 | const droidType = new GraphQLObjectType({
211 | name: 'Droid',
212 | description: 'A mechanical creature in the Star Wars universe.',
213 | fields: () => ({
214 | id: {
215 | type: new GraphQLNonNull(GraphQLString),
216 | description: 'The id of the droid.',
217 | },
218 | name: {
219 | type: GraphQLString,
220 | description: 'The name of the droid.',
221 | },
222 | friends: {
223 | type: new GraphQLList(characterInterface),
224 | description:
225 | 'The friends of the droid, or an empty list if they have none.',
226 | resolve: (droid) => getFriends(droid),
227 | },
228 | appearsIn: {
229 | type: new GraphQLList(episodeEnum),
230 | description: 'Which movies they appear in.',
231 | },
232 | secretBackstory: {
233 | type: GraphQLString,
234 | description: 'Construction date and the name of the designer.',
235 | resolve() {
236 | throw new Error('secretBackstory is secret.');
237 | },
238 | },
239 | primaryFunction: {
240 | type: GraphQLString,
241 | description: 'The primary function of the droid.',
242 | },
243 | }),
244 | interfaces: [characterInterface],
245 | });
246 |
247 | /**
248 | * This is the type that will be the root of our query, and the
249 | * entry point into our schema. It gives us the ability to fetch
250 | * objects by their IDs, as well as to fetch the undisputed hero
251 | * of the Star Wars trilogy, R2-D2, directly.
252 | *
253 | * This implements the following type system shorthand:
254 | * ```graphql
255 | * type Query {
256 | * hero(episode: Episode): Character
257 | * human(id: String!): Human
258 | * droid(id: String!): Droid
259 | * }
260 | * ```
261 | */
262 | const queryType = new GraphQLObjectType({
263 | name: 'Query',
264 | fields: () => ({
265 | hero: {
266 | type: characterInterface,
267 | args: {
268 | episode: {
269 | description:
270 | 'If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.',
271 | type: episodeEnum,
272 | },
273 | },
274 | resolve: (_source, { episode }) => getHero(episode),
275 | },
276 | human: {
277 | type: humanType,
278 | args: {
279 | id: {
280 | description: 'id of the human',
281 | type: new GraphQLNonNull(GraphQLString),
282 | },
283 | },
284 | resolve: (_source, args, info, context) => resolverCache.checkCache(_source, args, info, context, getHuman),
285 | },
286 | droid: {
287 | type: droidType,
288 | args: {
289 | id: {
290 | description: 'id of the droid',
291 | type: new GraphQLNonNull(GraphQLString),
292 | },
293 | },
294 | resolve: (_source, { id }) => getDroid(id),
295 | },
296 | }),
297 | });
298 |
299 | const mutationType = new GraphQLObjectType({
300 | name: 'Mutation',
301 | fields: () => ({
302 | updatehuman: {
303 | type: humanType,
304 | args: {
305 | id: {
306 | description: 'id of the human',
307 | type: new GraphQLNonNull(GraphQLString),
308 | },
309 | prop: {
310 | description: 'property to be updated',
311 | type: new GraphQLNonNull(GraphQLString),
312 | },
313 | update: {
314 | description: 'the new property value',
315 | type: new GraphQLNonNull(GraphQLString)
316 | },
317 | },
318 | resolve: (_source, args, info, context) => resolverCache.updateCache(_source, args, info, context, updateHuman),
319 | },
320 |
321 | }),
322 | });
323 | /**
324 | * Finally, we construct our schema (whose starting query type is the query
325 | * type we defined above) and export it.
326 | */
327 | export const StarWarsSchema: GraphQLSchema = new GraphQLSchema({
328 | query: queryType,
329 | mutation: mutationType,
330 | types: [humanType, droidType],
331 | });
--------------------------------------------------------------------------------
/src/SplacheCache.ts:
--------------------------------------------------------------------------------
1 | import { createClient, RedisClientType } from 'redis';
2 | import { parse, visit, GraphQLSchema } from 'graphql';
3 | import { graphql } from 'graphql';
4 |
5 | /* Creates an importable SplacheCache Class that accepts a GraphQL schema
6 | and connects to the user's local Redis client or provided Redis client*/
7 | export class SplacheCache {
8 | schema: GraphQLSchema;
9 | typeToFields: object;
10 | queryToReturnType: object;
11 | client: RedisClientType;
12 | constructor(
13 | schema: GraphQLSchema,
14 | host?: string,
15 | port?: number,
16 | password?: string
17 | ) {
18 | this.schema = schema;
19 | this.GQLquery = this.GQLquery.bind(this);
20 | this.typeToFields = typeToFields(this.schema);
21 | this.queryToReturnType = queryToReturnedType(this.schema);
22 | if (host && port && password) {
23 | this.client = createClient({
24 | socket: {
25 | host,
26 | port,
27 | },
28 | password,
29 | });
30 | } else if (host && port) {
31 | this.client = createClient({
32 | socket: {
33 | host,
34 | port,
35 | },
36 | });
37 | } else {
38 | this.client = createClient();
39 | }
40 | this.client.connect()
41 | .then(() => console.log('connected to redis'))
42 | .catch((err) => console.log(`there was a problem connecting to the redis instance: ${err}`))
43 | }
44 | //Partial normalization of the input query through traversing the GraphQL AST to build valid root queries.
45 | //The root queries are checked against the Redis cache as opposed to the original query
46 | async GQLquery(req: any, res: any, next: any) {
47 | const queryString: string = req.body.query;
48 | const ast = parse(queryString);
49 | const [template, fieldArgs] = await makeTemplate(ast);
50 | const splitQuery = qlStrGen(template, fieldArgs);
51 | const compiledObj = { data: {} };
52 | for (const query of splitQuery) {
53 | const isInCache = await this.client.EXISTS(query);
54 | if (isInCache) {
55 | const returnObj = await this.client.GET(query);
56 | if (typeof returnObj === 'string') {
57 | const returnObjParsed = JSON.parse(returnObj);
58 | compiledObj.data[Object.keys(returnObjParsed.data)[0]] =
59 | returnObjParsed.data[Object.keys(returnObjParsed.data)[0]];
60 | } else {
61 | return next({ err: 'There was a redis error' });
62 | }
63 | } else {
64 | await graphql({ schema: this.schema, source: query })
65 | .then((response: any) => {
66 | compiledObj.data[Object.keys(response.data)[0]] =
67 | response.data[Object.keys(response.data)[0]];
68 | this.client.SET(query, JSON.stringify(response));
69 | })
70 | .catch((err) => next({ err }));
71 | }
72 | }
73 | res.locals.queryResult = compiledObj;
74 | return next();
75 | }
76 | }
77 |
78 | // makeTemplate uses the AST from the GraphQL query as an input
79 | // The visit function is provided by graphql, check documentation here https://graphql.org/graphql-js/language/#visit
80 | export async function makeTemplate(ast: any) {
81 | const template: any = {};
82 | const path: any = [];
83 | const fieldInfo: any = {};
84 | visit(ast, {
85 | Field: {
86 | enter(node: any) {
87 | if (node.arguments) {
88 | const args: any = {};
89 | for (let i = 0; i < node.arguments.length; i++) {
90 | const key = node.arguments[i].name.value;
91 | args[key] = node.arguments[i].value.value;
92 | }
93 | fieldInfo[node.name.value] = { ...args };
94 | }
95 |
96 | path.push(node.name.value);
97 | },
98 | leave() {
99 | path.pop();
100 | },
101 | },
102 | SelectionSet: {
103 | enter(node: any, key, parent: any) {
104 | if (parent.kind === 'Field') {
105 | const fields = {};
106 | for (let i = 0; i < node.selections.length; i++) {
107 | if (!node.selections[i].selectionSet) {
108 | fields[node.selections[i].name.value] = true;
109 | }
110 | }
111 | const fieldsObj = {
112 | ...fields,
113 | ...fieldInfo[path[path.length - 1]],
114 | };
115 | let curr = template;
116 | for (let i = 0; i < path.length; i++) {
117 | if (i + 1 === path.length) curr[path[i]] = { ...fieldsObj };
118 | curr = curr[path[i]];
119 | }
120 | }
121 | },
122 | leave() {
123 | path.pop();
124 | },
125 | },
126 | });
127 | return [template, fieldInfo];
128 | }
129 |
130 | export function typeToFields(schema: GraphQLSchema) {
131 | const builtInTypes = {
132 | String: 'String',
133 | Int: 'Int',
134 | Float: 'Float',
135 | Boolean: 'Boolean',
136 | ID: 'ID',
137 | Query: 'Query',
138 | __Type: '__Type',
139 | __Field: '__Field',
140 | __EnumValue: '__EnumValue',
141 | __DirectiveLocation: '__DirectiveLocation',
142 | __Schema: '__Schema',
143 | __TypeKind: '__TypeKind',
144 | __InputValue: '__InputValue',
145 | __Directive: '__Directive',
146 | };
147 | const typeMap: any = schema.getTypeMap();
148 | const typesToFields = {};
149 | for (const type in typeMap) {
150 | if (type in builtInTypes === false) {
151 | const tempObj = {};
152 | const fields = typeMap[type]._fields;
153 | for (const field in fields) {
154 | const key = fields[field].name.toLowerCase();
155 | let value: any;
156 | if (fields[field].type.ofType)
157 | value = fields[field].type.ofType.name.toLowerCase();
158 | else value = fields[field].type.name.toLowerCase();
159 | tempObj[key] = value;
160 | }
161 | typesToFields[type.toLowerCase()] = tempObj;
162 | }
163 | }
164 | return typesToFields;
165 | }
166 |
167 | export function queryToReturnedType(schema: GraphQLSchema) {
168 | const queryTypeFields: any = schema.getQueryType()?.getFields();
169 | const map: any = {};
170 | for (const key in queryTypeFields) {
171 | if (queryTypeFields[key].type._interfaces.length > 0)
172 | map[key] = queryTypeFields[key].type._interfaces[0].name.toLowerCase();
173 | else map[key] = queryTypeFields[key].type.name.toLowerCase();
174 | }
175 | return map;
176 | }
177 |
178 | //GQLquery helper functions below
179 | export function qlStrGen(template, fieldArgs) {
180 | const queryStrs: string[] = [];
181 | for (const prop in template) {
182 | let queryStr = `query {${prop} ${genArgStr(fieldArgs[prop])} {`;
183 | queryStr += genfields(template[prop]);
184 | queryStrs.push((queryStr += '} }'));
185 | }
186 | return queryStrs;
187 | }
188 |
189 | export function genArgStr(args) {
190 | if (Object.keys(args)[0] === undefined) return '';
191 | let argStr = '(';
192 | for (const arg in args) {
193 | argStr += `${arg}: "${args[arg]}" `;
194 | }
195 | return (argStr += ')');
196 | }
197 |
198 | export function genfields(fields) {
199 | let fieldStr = '';
200 | for (const field in fields) {
201 | if (typeof fields[field] === 'object') {
202 | fieldStr += `${field} {${genfields(fields[field])}} `;
203 | } else {
204 | if (field !== 'id') fieldStr += `${field} `;
205 | }
206 | }
207 | return fieldStr;
208 | }
209 |
--------------------------------------------------------------------------------
/src/__test__/CacheWhole.test.ts:
--------------------------------------------------------------------------------
1 | import { SplacheCacheWhole } from "../CacheWhole";
2 | import { Request, Response } from "express";
3 | import {StarWarsSchema} from '../schemaTest'
4 | describe("SplacheCacheWhole", () => {
5 |
6 | let mockRequest: Partial;
7 | let mockResponse: Partial;
8 |
9 | beforeEach(()=>{
10 | mockRequest = {};
11 | mockResponse = {locals: {}};
12 | })
13 |
14 | const testSplacheCacheWhole = new SplacheCacheWhole(StarWarsSchema);
15 | afterAll(()=>{
16 | testSplacheCacheWhole.client.QUIT();
17 | })
18 | it('.wholeCache should return next', async () => {
19 | mockRequest = {body:{query:
20 | `query FetchLukeAndC3POQuery {
21 | human(id: "1000") {
22 | id
23 | name
24 | friends{
25 | id
26 | name
27 | }
28 | }
29 | droid(id: "2000") {
30 | id
31 | name
32 | }`
33 |
34 | } } as Request;
35 | const next = jest.fn();
36 | await testSplacheCacheWhole.wholeCache(mockRequest, mockResponse, next);
37 | expect(next).toHaveBeenCalled();
38 | })
39 | it('.wholeCache should call client.set if the key does not exist', async () => {
40 | await testSplacheCacheWhole.client.FLUSHALL();
41 | const spy = jest.spyOn(testSplacheCacheWhole.client, "SET");
42 | mockRequest = {body:{query:
43 | `query FetchLukeAndC3POQuery {
44 | human(id: "1000") {
45 | id
46 | name
47 | friends{
48 | id
49 | name
50 | }
51 | }
52 | `
53 |
54 | } } as Request;
55 | const next = jest.fn();
56 | await testSplacheCacheWhole.wholeCache(mockRequest, mockResponse, next);
57 | expect(spy).toHaveBeenCalled();
58 | })
59 | it('.wholeCache should call client.get if the key already exists', async () => {
60 | const spy = jest.spyOn(testSplacheCacheWhole.client, "SET");
61 | mockRequest = {body:{query:
62 | `query FetchLukeAndC3POQuery {
63 | human(id: "1000") {
64 | id
65 | name
66 | friends{
67 | id
68 | name
69 | }
70 | }
71 | `
72 |
73 | } } as Request;
74 | const next = jest.fn();
75 | await testSplacheCacheWhole.wholeCache(mockRequest, mockResponse, next);
76 | expect(spy).toHaveBeenCalled();
77 | })
78 | })
79 |
80 |
--------------------------------------------------------------------------------
/src/__test__/RedisConnection.test.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from 'redis'
2 | import { ResolverCache } from "../ResolverCache";
3 |
4 |
5 | interface MockRedisClient {
6 | exists: jest.Mock, [string]>;
7 | get: jest.Mock, [string]>;
8 | set: jest.Mock, [string, string]>;
9 | connect: jest.Mock, []>;
10 | }
11 |
12 | const createMockRedisClient = (): MockRedisClient => ({
13 | exists: jest.fn(),
14 | get: jest.fn(),
15 | set: jest.fn(),
16 | connect: jest.fn(),
17 | });
18 |
19 | jest.mock('redis');
20 |
21 |
22 | const mockCreateClient = createClient as jest.MockedFunction;
23 |
24 | describe ('ResolverCache Redis Connection', () => {
25 | let mockClient: MockRedisClient;
26 | let resolverCache: ResolverCache;
27 |
28 | beforeEach(() => {
29 | mockClient = createMockRedisClient();
30 | resolverCache = new ResolverCache('localhost', 6379, 'password');
31 | resolverCache.client = mockClient as any;
32 | });
33 |
34 | afterEach(() => {
35 | jest.clearAllMocks();
36 | });
37 |
38 | describe('constructor', () => {
39 | it('should create a new Redis client with default options if no arguments are provided', () => {
40 | resolverCache = new ResolverCache();
41 | expect(mockCreateClient).toHaveBeenCalledTimes(1);
42 | });
43 |
44 | it('should create a new Redis client with host and port if provided', () => {
45 | resolverCache = new ResolverCache('localhost', 6379);
46 | expect(mockCreateClient).toHaveBeenCalledWith({ socket: { host: 'localhost', port: 6379 } });
47 | });
48 |
49 | it('should create a new Redis client with host, port and password if provided', () => {
50 | resolverCache = new ResolverCache('localhost', 6379, 'password');
51 | expect(mockCreateClient).toHaveBeenCalledWith({ socket: { host: 'localhost', port: 6379 }, password: 'password' });
52 | });
53 |
54 | it('should call the connect method of the Redis client', async () => {
55 | await resolverCache.client.connect();
56 | expect(resolverCache.client.connect).toHaveBeenCalledTimes(1);
57 | });
58 | });
59 | })
--------------------------------------------------------------------------------
/src/__test__/Resolver.test.ts:
--------------------------------------------------------------------------------
1 | import { ResolverCache } from "../ResolverCache";
2 |
3 | //must have redis instance running and flushed
4 |
5 |
6 | describe("ResolverCache", () => {
7 |
8 | function testFunc (args: any){
9 | return 0
10 | }
11 | const testResolverCache = new ResolverCache()
12 | afterAll(()=>{
13 | testResolverCache.client.QUIT();
14 | })
15 |
16 | it('checkCache should be a function', async ()=> {
17 | expect(typeof testResolverCache.checkCache).toEqual('function')
18 | })
19 | it('checkCache should return the result of the callback', async ()=> {
20 | const result = await testResolverCache.checkCache({}, {}, {}, {returnType:'test'}, testFunc)
21 | expect(result).toEqual(0)
22 | })
23 | it('checkCache should call client.set if the key doesnt exist', async()=>{
24 | await testResolverCache.client.FLUSHALL();
25 | const spy = jest.spyOn(testResolverCache.client, "SET");
26 | await testResolverCache.checkCache({}, {id:'1'}, {}, {returnType:'test'}, testFunc)
27 | expect(spy).toBeCalled();
28 | })
29 | it('checkCache should call client.get if the key exists', async()=>{
30 | const spy = jest.spyOn(testResolverCache.client, "GET");
31 | await testResolverCache.checkCache({}, {id:'1'}, {}, {returnType:'test'}, testFunc)
32 | expect(spy).toBeCalled();
33 | })
34 | it('updateCache should return the result of the callback', async ()=> {
35 | const result = await testResolverCache.updateCache({}, {}, {}, {returnType:'test'}, testFunc)
36 | expect(result).toEqual(0)
37 | })
38 | it('updateCache should call client.set', async()=>{
39 | const spy = jest.spyOn(testResolverCache.client, "SET");
40 | await testResolverCache.checkCache({}, {id:'1'}, {}, {returnType:'test'}, testFunc)
41 | expect(spy).toBeCalled
42 | })
43 | })
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/__test__/SplacheCache.test.ts:
--------------------------------------------------------------------------------
1 | import { SplacheCache } from "../SplacheCache";
2 | import {StarWarsSchema} from '../schemaTest';
3 | import { Request, Response } from "express";
4 |
5 | describe ("SplacheCache", ()=>{
6 |
7 | const testSplacheCache = new SplacheCache(StarWarsSchema);
8 |
9 | let mockRequest: Partial;
10 | let mockResponse: any;
11 |
12 | beforeEach(()=>{
13 | mockRequest = {};
14 | mockResponse = {locals: {}};
15 | })
16 | afterEach(() => {
17 | jest.clearAllMocks();
18 | });
19 | afterAll(()=>{
20 | testSplacheCache.client.QUIT();
21 | })
22 |
23 | it(".GQLquery should attach the correct response to res.locals for one root query", async ()=>{
24 | const expectedResponse = {
25 | "data": {
26 | "human": {
27 | "id": "1000",
28 | "name": "Luke Skywalker"
29 | }
30 | }
31 | }
32 | mockRequest = {
33 | body:{
34 | query: `{
35 | human (id:"1000") {
36 | name
37 | }
38 | }`
39 | }
40 | }
41 | const next = jest.fn();
42 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
43 | expect(mockResponse.locals.queryResult).toEqual(expectedResponse);
44 | })
45 |
46 | it(".GQLquery should attach the correct response to res.locals for multiple root queries", async ()=>{
47 | const expectedResponse = {
48 | "data": {
49 | "human": {
50 | "id": "1000",
51 | "name": "Luke Skywalker"
52 | },
53 | "droid": {
54 | "name": "C-3PO",
55 | "id": "2000"
56 | },
57 | "hero": {
58 | "name": "R2-D2"
59 | }
60 | }
61 | }
62 | mockRequest = {
63 | body:{
64 | query: `{
65 | human (id:"1000") {
66 | name
67 | }
68 | droid (id:"2000") {
69 | name
70 | }
71 | hero {
72 | name
73 | }
74 | }`
75 | }
76 | }
77 | const next = jest.fn();
78 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
79 | expect(mockResponse.locals.queryResult).toEqual(expectedResponse);
80 | })
81 | it('.GQLquery should handle queries where there are nested fields', async () => {
82 | const expectedResponse = {
83 | "data": {
84 | "human": {
85 | "id": "1000",
86 | "name": "Luke Skywalker",
87 | "friends": [
88 | {
89 | "id": "1002",
90 | "name": "Han Solo",
91 | "friends": [
92 | {
93 | "name": "Luke Skywalker"
94 | },
95 | {
96 | "name": "Leia Organa"
97 | },
98 | {
99 | "name": "R2-D2"
100 | }
101 | ]
102 | },
103 | {
104 | "id": "1003",
105 | "name": "Leia Organa",
106 | "friends": [
107 | {
108 | "name": "Luke Skywalker"
109 | },
110 | {
111 | "name": "Han Solo"
112 | },
113 | {
114 | "name": "C-3PO"
115 | },
116 | {
117 | "name": "R2-D2"
118 | }
119 | ]
120 | },
121 | {
122 | "id": "2000",
123 | "name": "C-3PO",
124 | "friends": [
125 | {
126 | "name": "Luke Skywalker"
127 | },
128 | {
129 | "name": "Han Solo"
130 | },
131 | {
132 | "name": "Leia Organa"
133 | },
134 | {
135 | "name": "R2-D2"
136 | }
137 | ]
138 | },
139 | {
140 | "id": "2001",
141 | "name": "R2-D2",
142 | "friends": [
143 | {
144 | "name": "Luke Skywalker"
145 | },
146 | {
147 | "name": "Han Solo"
148 | },
149 | {
150 | "name": "Leia Organa"
151 | }
152 | ]
153 | }
154 | ]
155 | },
156 | "droid": {
157 | "name": "C-3PO",
158 | "id": "2000"
159 | },
160 | "hero": {
161 | "name": "R2-D2"
162 | }
163 | }
164 | }
165 | mockRequest = {
166 | body:{
167 | query: `query {
168 | human (id:"1000") {
169 | id
170 | name
171 | friends {
172 | id
173 | name
174 | friends {
175 | name
176 | }
177 | }
178 | }
179 | droid (id: "2000") {
180 | name
181 | }
182 | hero {
183 | name
184 | }
185 | }`
186 | }
187 | }
188 | const next = jest.fn();
189 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
190 | expect(mockResponse.locals.queryResult).toEqual(expectedResponse);
191 | });
192 | it(".GQLquery should check the cache for the key on every invocation", async ()=>{
193 | const spy = jest.spyOn(testSplacheCache.client, "EXISTS")
194 | const next = jest.fn();
195 | mockRequest = {
196 | body:{
197 | query: `{
198 | human (id:"1000") {
199 | name
200 | }
201 | }`
202 | }
203 | }
204 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
205 | expect(spy).toBeCalled();
206 | })
207 | it(".GQLquery should call client.set if the key doesn't exist in the cache", async ()=>{
208 | await testSplacheCache.client.FLUSHALL()
209 | const spy = jest.spyOn(testSplacheCache.client, "SET")
210 | const next = jest.fn();
211 | mockRequest = {
212 | body:{
213 | query: `{
214 | human (id:"1000") {
215 | name
216 | }
217 | }`
218 | }
219 | }
220 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
221 | expect(spy).toBeCalled();
222 | })
223 | it(".GQLquery should call client.get if the key is found in the cache", async ()=> {
224 | const spy = jest.spyOn(testSplacheCache.client, "GET")
225 | const next = jest.fn();
226 | mockRequest = {
227 | body:{
228 | query: `{
229 | human (id:"1000") {
230 | name
231 | }
232 | }`
233 | }
234 | }
235 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
236 | expect(spy).toBeCalled();
237 |
238 | })
239 | it(".GQLquery should retrieve query results from both cache and graphQL query when partial caching has occured", async () => {
240 | const spySET = jest.spyOn(testSplacheCache.client, "SET")
241 | const spyGET = jest.spyOn(testSplacheCache.client, "GET")
242 | const next = jest.fn();
243 | mockRequest = {
244 | body:{
245 | query: `{
246 | human (id:"1000") {
247 | name
248 | }
249 | droid (id: "2000") {
250 | name
251 | }
252 | }`
253 | }
254 | }
255 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
256 | expect(spySET).toBeCalled();
257 | expect(spyGET).toBeCalled();
258 | })
259 | it(".GQLquery should retrieve root queries that have been stored in the cache as partials", async () => {
260 | const spySet = jest.spyOn(testSplacheCache.client, "SET")
261 | mockRequest = {
262 | body:{
263 | query: `{
264 | human (id:"1000") {
265 | name
266 | }
267 | }`
268 | }
269 | }
270 | const next = jest.fn();
271 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
272 | mockRequest = {
273 | body:{
274 | query: `{
275 | droid (id: "2000") {
276 | name
277 | }
278 | }`
279 | }
280 | }
281 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
282 | expect(spySet).toHaveBeenCalledTimes(0);
283 |
284 | })
285 | it(".GQLquery should produce complex queries from cached root queries", async () => {
286 | const spySet = jest.spyOn(testSplacheCache.client, "SET")
287 | const next = jest.fn();
288 | mockRequest = {
289 | body:{
290 | query: `{
291 | human (id:"1000") {
292 | name
293 | }
294 | droid (id: "2000") {
295 | name
296 | }
297 | }`
298 | }
299 | }
300 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next)
301 | expect(spySet).toHaveBeenCalledTimes(0);
302 | })
303 | it(".GQLquery should return next", async ()=>{
304 | mockRequest = {body:{query:
305 | `query FetchLukeAndC3POQuery {
306 | human(id: "1000") {
307 | id
308 | name
309 | friends{
310 | id
311 | name
312 | }
313 | }
314 | droid(id: "2000") {
315 | id
316 | name
317 | }
318 | }`
319 |
320 | } } as Request;
321 | const next = jest.fn();
322 | await testSplacheCache.GQLquery(mockRequest, mockResponse, next);
323 | expect(next).toHaveBeenCalled();
324 | })
325 | })
--------------------------------------------------------------------------------
/src/__test__/SplacheCacheHelpers.test.ts:
--------------------------------------------------------------------------------
1 | import { SplacheCache, makeTemplate, typeToFields, qlStrGen, genArgStr, genfields, queryToReturnedType} from "../SplacheCache";
2 | import { Request, Response } from "express";
3 | import {StarWarsSchema} from '../schemaTest'
4 | import {parse} from 'graphql'
5 | //must have redis instance running and flushed
6 |
7 |
8 |
9 | describe("SplacheCache Helpers", () => {
10 |
11 | it('makeTemplate should output a valid template, and fieldArgs', async ()=> {
12 | const queryString = `query FetchLukeAndC3POQuery {
13 | human(id: "1000") {
14 | name
15 | }
16 | droid(id: "2000") {
17 | name
18 | }
19 | }`
20 | const ast = parse(queryString)
21 | const output = await makeTemplate(ast)
22 | const [template, fieldArgs] = output;
23 | expect(template).toEqual({
24 | human: { name: true, id: '1000' },
25 | droid: { name: true, id: '2000' }
26 | })
27 | expect(fieldArgs).toEqual({ human: { id: '1000' }, name: {}, droid: { id: '2000' } })
28 | })
29 |
30 | it('typeToFields should output a valid type map (object)', () => {
31 | const expectedTypeMap = {
32 | human: {
33 | id: 'string',
34 | name: 'string',
35 | friends: 'character',
36 | appearsin: 'episode',
37 | homeplanet: 'string',
38 | secretbackstory: 'string'
39 | },
40 | character: {
41 | id: 'string',
42 | name: 'string',
43 | friends: 'character',
44 | appearsin: 'episode',
45 | secretbackstory: 'string'
46 | },
47 | episode: {},
48 | droid: {
49 | id: 'string',
50 | name: 'string',
51 | friends: 'character',
52 | appearsin: 'episode',
53 | secretbackstory: 'string',
54 | primaryfunction: 'string'
55 | }
56 | }
57 |
58 | const output = typeToFields(StarWarsSchema)
59 | expect(output).toEqual(expectedTypeMap);
60 |
61 | });
62 | it('queryToReturned type should return a valid query to type map', async ()=> {
63 | const expectedMap = { hero: 'character', human: 'character', droid: 'character' }
64 | const output = queryToReturnedType(StarWarsSchema);
65 | expect(output).toEqual(expectedMap);
66 | });
67 | it('qlStrGen should return an array of single root graphQL queries', async () => {
68 | const expectedQueryStrings = [
69 | 'query {human (id: "1000" ) {id friends {id name friends {name } } } }',
70 | 'query {droid (id: "2000" ) {name id } }',
71 | 'query {hero {name } }'
72 | ];
73 | const queryStr = `query {
74 | human (id:"1000") {
75 | friends {
76 | id
77 | name
78 | friends {
79 | name
80 | }
81 | }
82 | }
83 | droid (id: "2000") {
84 | name
85 | }
86 | hero {
87 | name
88 | }
89 | }`
90 | const ast = parse(queryStr)
91 | const mkTemplateOut = await makeTemplate(ast)
92 | const [template, fieldArgs] = mkTemplateOut;
93 | const outputStrs = qlStrGen(template, fieldArgs);
94 | expect(outputStrs).toEqual(expectedQueryStrings);
95 | });
96 | it('genArgStr should return a string of the query arguments', ()=> {
97 | const qArgs = { id: '1000' , name: 'test'}
98 | const expectedQArgStr = '(id: "1000" name: "test" )'
99 | const outputArgStr = genArgStr(qArgs);
100 | expect(outputArgStr).toEqual(expectedQArgStr);
101 |
102 | });
103 | it('genfields should return a string of all query fields', () => {
104 | const qFields = {id: '1000',friends: { id: true, name: true, friends: { name: true } }}
105 | const expectQStr = 'id friends {id name friends {name } } '
106 | const outputQfield = genfields(qFields);
107 | expect(outputQfield).toEqual(expectQStr);
108 |
109 |
110 | })
111 |
112 | })
--------------------------------------------------------------------------------
/src/__test__/docker-compose-test.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | redis:
5 | image: redis
6 | ports:
7 | - "6379:6379"
--------------------------------------------------------------------------------
/src/index-test.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { SplacheCache } from './SplacheCache';
3 | import {SplacheCacheWhole} from './CacheWhole'
4 | import {StarWarsSchema} from './schemaTest'
5 | // import {StarWarsSchema} from './SchemaWithCache'
6 | import {graphql, GraphQLSchema} from 'graphql'
7 |
8 | const app = express()
9 |
10 | const cache = new SplacheCacheWhole(StarWarsSchema);
11 | // const cache = new SplacheCache(StarWarsSchema)
12 | app.use(express.json())
13 | app.use('/graphql', cache.wholeCache, (req, res) => {
14 | res.send(res.locals.queryResult)
15 | })
16 |
17 | // app.use('/graphql', cache.GQLquery, (req, res) => {
18 | // res.send(res.locals.queryResult)
19 | // })
20 | // app.use('/graphql', (req, res) => {
21 | // graphql({ schema: StarWarsSchema, source: req.body.query})
22 | // .then((response) => {
23 | // res.send(response)
24 | // })
25 | // })
26 | app.use('/graphql', (req, res) => {
27 | graphql({ schema: StarWarsSchema, source: req.body.query})
28 | .then((response) => {
29 | res.send(response)
30 | })
31 | })
32 |
33 | app.listen(4000)
--------------------------------------------------------------------------------
/src/schemaTest.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GraphQLEnumType,
3 | GraphQLInterfaceType,
4 | GraphQLList,
5 | GraphQLNonNull,
6 | GraphQLObjectType,
7 | } from 'graphql';
8 | import { GraphQLString } from 'graphql';
9 | import { GraphQLSchema } from 'graphql';
10 |
11 | import { getDroid, getFriends, getHero, getHuman } from './starWarsData';
12 |
13 | /**
14 | * This is designed to be an end-to-end test, demonstrating
15 | * the full GraphQL stack.
16 | *
17 | * We will create a GraphQL schema that describes the major
18 | * characters in the original Star Wars trilogy.
19 | *
20 | * NOTE: This may contain spoilers for the original Star
21 | * Wars trilogy.
22 | */
23 |
24 | /**
25 | * Using our shorthand to describe type systems, the type system for our
26 | * Star Wars example is:
27 | *
28 | * ```graphql
29 | * enum Episode { NEW_HOPE, EMPIRE, JEDI }
30 | *
31 | * interface Character {
32 | * id: String!
33 | * name: String
34 | * friends: [Character]
35 | * appearsIn: [Episode]
36 | * }
37 | *
38 | * type Human implements Character {
39 | * id: String!
40 | * name: String
41 | * friends: [Character]
42 | * appearsIn: [Episode]
43 | * homePlanet: String
44 | * }
45 | *
46 | * type Droid implements Character {
47 | * id: String!
48 | * name: String
49 | * friends: [Character]
50 | * appearsIn: [Episode]
51 | * primaryFunction: String
52 | * }
53 | *
54 | * type Query {
55 | * hero(episode: Episode): Character
56 | * human(id: String!): Human
57 | * droid(id: String!): Droid
58 | * }
59 | * ```
60 | *
61 | * We begin by setting up our schema.
62 | */
63 |
64 | /**
65 | * The original trilogy consists of three movies.
66 | *
67 | * This implements the following type system shorthand:
68 | * ```graphql
69 | * enum Episode { NEW_HOPE, EMPIRE, JEDI }
70 | * ```
71 | */
72 | const episodeEnum = new GraphQLEnumType({
73 | name: 'Episode',
74 | description: 'One of the films in the Star Wars Trilogy',
75 | values: {
76 | NEW_HOPE: {
77 | value: 4,
78 | description: 'Released in 1977.',
79 | },
80 | EMPIRE: {
81 | value: 5,
82 | description: 'Released in 1980.',
83 | },
84 | JEDI: {
85 | value: 6,
86 | description: 'Released in 1983.',
87 | },
88 | },
89 | });
90 |
91 | /**
92 | * Characters in the Star Wars trilogy are either humans or droids.
93 | *
94 | * This implements the following type system shorthand:
95 | * ```graphql
96 | * interface Character {
97 | * id: String!
98 | * name: String
99 | * friends: [Character]
100 | * appearsIn: [Episode]
101 | * secretBackstory: String
102 | * }
103 | * ```
104 | */
105 | const characterInterface: GraphQLInterfaceType = new GraphQLInterfaceType({
106 | name: 'Character',
107 | description: 'A character in the Star Wars Trilogy',
108 | fields: () => ({
109 | id: {
110 | type: new GraphQLNonNull(GraphQLString),
111 | description: 'The id of the character.',
112 | },
113 | name: {
114 | type: GraphQLString,
115 | description: 'The name of the character.',
116 | },
117 | friends: {
118 | type: new GraphQLList(characterInterface),
119 | description:
120 | 'The friends of the character, or an empty list if they have none.',
121 | },
122 | appearsIn: {
123 | type: new GraphQLList(episodeEnum),
124 | description: 'Which movies they appear in.',
125 | },
126 | secretBackstory: {
127 | type: GraphQLString,
128 | description: 'All secrets about their past.',
129 | },
130 | }),
131 | resolveType(character) {
132 | switch (character.type) {
133 | case 'Human':
134 | return humanType.name;
135 | case 'Droid':
136 | return droidType.name;
137 | }
138 | },
139 | });
140 |
141 | /**
142 | * We define our human type, which implements the character interface.
143 | *
144 | * This implements the following type system shorthand:
145 | * ```graphql
146 | * type Human : Character {
147 | * id: String!
148 | * name: String
149 | * friends: [Character]
150 | * appearsIn: [Episode]
151 | * secretBackstory: String
152 | * }
153 | * ```
154 | */
155 | const humanType = new GraphQLObjectType({
156 | name: 'Human',
157 | description: 'A humanoid creature in the Star Wars universe.',
158 | fields: () => ({
159 | id: {
160 | type: new GraphQLNonNull(GraphQLString),
161 | description: 'The id of the human.',
162 | },
163 | name: {
164 | type: GraphQLString,
165 | description: 'The name of the human.',
166 | },
167 | friends: {
168 | type: new GraphQLList(characterInterface),
169 | description:
170 | 'The friends of the human, or an empty list if they have none.',
171 | resolve: (human) => getFriends(human),
172 | },
173 | appearsIn: {
174 | type: new GraphQLList(episodeEnum),
175 | description: 'Which movies they appear in.',
176 | },
177 | homePlanet: {
178 | type: GraphQLString,
179 | description: 'The home planet of the human, or null if unknown.',
180 | },
181 | secretBackstory: {
182 | type: GraphQLString,
183 | description: 'Where are they from and how they came to be who they are.',
184 | resolve() {
185 | throw new Error('secretBackstory is secret.');
186 | },
187 | },
188 | }),
189 | interfaces: [characterInterface],
190 | });
191 |
192 | /**
193 | * The other type of character in Star Wars is a droid.
194 | *
195 | * This implements the following type system shorthand:
196 | * ```graphql
197 | * type Droid : Character {
198 | * id: String!
199 | * name: String
200 | * friends: [Character]
201 | * appearsIn: [Episode]
202 | * secretBackstory: String
203 | * primaryFunction: String
204 | * }
205 | * ```
206 | */
207 | const droidType = new GraphQLObjectType({
208 | name: 'Droid',
209 | description: 'A mechanical creature in the Star Wars universe.',
210 | fields: () => ({
211 | id: {
212 | type: new GraphQLNonNull(GraphQLString),
213 | description: 'The id of the droid.',
214 | },
215 | name: {
216 | type: GraphQLString,
217 | description: 'The name of the droid.',
218 | },
219 | friends: {
220 | type: new GraphQLList(characterInterface),
221 | description:
222 | 'The friends of the droid, or an empty list if they have none.',
223 | resolve: (droid) => getFriends(droid),
224 | },
225 | appearsIn: {
226 | type: new GraphQLList(episodeEnum),
227 | description: 'Which movies they appear in.',
228 | },
229 | secretBackstory: {
230 | type: GraphQLString,
231 | description: 'Construction date and the name of the designer.',
232 | resolve() {
233 | throw new Error('secretBackstory is secret.');
234 | },
235 | },
236 | primaryFunction: {
237 | type: GraphQLString,
238 | description: 'The primary function of the droid.',
239 | },
240 | }),
241 | interfaces: [characterInterface],
242 | });
243 |
244 | /**
245 | * This is the type that will be the root of our query, and the
246 | * entry point into our schema. It gives us the ability to fetch
247 | * objects by their IDs, as well as to fetch the undisputed hero
248 | * of the Star Wars trilogy, R2-D2, directly.
249 | *
250 | * This implements the following type system shorthand:
251 | * ```graphql
252 | * type Query {
253 | * hero(episode: Episode): Character
254 | * human(id: String!): Human
255 | * droid(id: String!): Droid
256 | * }
257 | * ```
258 | */
259 | const queryType = new GraphQLObjectType({
260 | name: 'Query',
261 | fields: () => ({
262 | hero: {
263 | type: characterInterface,
264 | args: {
265 | episode: {
266 | description:
267 | 'If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.',
268 | type: episodeEnum,
269 | },
270 | },
271 | resolve: (_source, { episode }) => getHero(episode),
272 | },
273 | human: {
274 | type: humanType,
275 | args: {
276 | id: {
277 | description: 'id of the human',
278 | type: new GraphQLNonNull(GraphQLString),
279 | },
280 | },
281 | resolve: (_source, { id }) => getHuman(id),
282 | },
283 | droid: {
284 | type: droidType,
285 | args: {
286 | id: {
287 | description: 'id of the droid',
288 | type: new GraphQLNonNull(GraphQLString),
289 | },
290 | },
291 | resolve: (_source, { id }) => getDroid(id),
292 | },
293 | }),
294 | });
295 |
296 | /**
297 | * Finally, we construct our schema (whose starting query type is the query
298 | * type we defined above) and export it.
299 | */
300 | export const StarWarsSchema: GraphQLSchema = new GraphQLSchema({
301 | query: queryType,
302 | types: [humanType, droidType],
303 | });
--------------------------------------------------------------------------------
/src/starWarsData.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * These are types which correspond to the schema.
3 | * They represent the shape of the data visited during field resolution.
4 | */
5 | export interface Character {
6 | id: string;
7 | name: string;
8 | friends: ReadonlyArray;
9 | appearsIn: ReadonlyArray;
10 | }
11 |
12 | export interface Human {
13 | type: 'Human';
14 | id: string;
15 | name: string;
16 | friends: ReadonlyArray;
17 | appearsIn: ReadonlyArray;
18 | homePlanet?: string;
19 | }
20 |
21 | export interface Droid {
22 | type: 'Droid';
23 | id: string;
24 | name: string;
25 | friends: ReadonlyArray;
26 | appearsIn: ReadonlyArray;
27 | primaryFunction: string;
28 | }
29 |
30 | /**
31 | * This defines a basic set of data for our Star Wars Schema.
32 | *
33 | * This data is hard coded for the sake of the demo, but you could imagine
34 | * fetching this data from a backend service rather than from hardcoded
35 | * JSON objects in a more complex demo.
36 | */
37 |
38 | const luke: Human = {
39 | type: 'Human',
40 | id: '1000',
41 | name: 'Luke Skywalker',
42 | friends: ['1002', '1003', '2000', '2001'],
43 | appearsIn: [4, 5, 6],
44 | homePlanet: 'Tatooine',
45 | };
46 |
47 | const vader: Human = {
48 | type: 'Human',
49 | id: '1001',
50 | name: 'Darth Vader',
51 | friends: ['1004'],
52 | appearsIn: [4, 5, 6],
53 | homePlanet: 'Tatooine',
54 | };
55 |
56 | const han: Human = {
57 | type: 'Human',
58 | id: '1002',
59 | name: 'Han Solo',
60 | friends: ['1000', '1003', '2001'],
61 | appearsIn: [4, 5, 6],
62 | };
63 |
64 | const leia: Human = {
65 | type: 'Human',
66 | id: '1003',
67 | name: 'Leia Organa',
68 | friends: ['1000', '1002', '2000', '2001'],
69 | appearsIn: [4, 5, 6],
70 | homePlanet: 'Alderaan',
71 | };
72 |
73 | const tarkin: Human = {
74 | type: 'Human',
75 | id: '1004',
76 | name: 'Wilhuff Tarkin',
77 | friends: ['1001'],
78 | appearsIn: [4],
79 | };
80 |
81 | const humanData: { [id: string]: Human } = {
82 | [luke.id]: luke,
83 | [vader.id]: vader,
84 | [han.id]: han,
85 | [leia.id]: leia,
86 | [tarkin.id]: tarkin,
87 | };
88 |
89 | const threepio: Droid = {
90 | type: 'Droid',
91 | id: '2000',
92 | name: 'C-3PO',
93 | friends: ['1000', '1002', '1003', '2001'],
94 | appearsIn: [4, 5, 6],
95 | primaryFunction: 'Protocol',
96 | };
97 |
98 | const artoo: Droid = {
99 | type: 'Droid',
100 | id: '2001',
101 | name: 'R2-D2',
102 | friends: ['1000', '1002', '1003'],
103 | appearsIn: [4, 5, 6],
104 | primaryFunction: 'Astromech',
105 | };
106 |
107 | const droidData: { [id: string]: Droid } = {
108 | [threepio.id]: threepio,
109 | [artoo.id]: artoo,
110 | };
111 |
112 | /**
113 | * Helper function to get a character by ID.
114 | */
115 | function getCharacter(id: string): Promise {
116 | // Returning a promise just to illustrate that GraphQL.js supports it.
117 | return Promise.resolve(humanData[id] ?? droidData[id]);
118 | }
119 |
120 | /**
121 | * Allows us to query for a character's friends.
122 | */
123 | export function getFriends(
124 | character: Character,
125 | ): Array> {
126 | // Notice that GraphQL accepts Arrays of Promises.
127 | return character.friends.map((id) => getCharacter(id));
128 | }
129 |
130 | /**
131 | * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2.
132 | */
133 | export function getHero(episode: number): Character {
134 | if (episode === 5) {
135 | // Luke is the hero of Episode V.
136 | return luke;
137 | }
138 | // Artoo is the hero otherwise.
139 | return artoo;
140 | }
141 |
142 | /**
143 | * Allows us to query for the human with the given id.
144 | */
145 | export function getHuman(id: string): Human | null {
146 | return humanData[id];
147 | }
148 |
149 | /**
150 | * Allows us to query for the droid with the given id.
151 | */
152 | export function getDroid(id: string): Droid | null {
153 | return droidData[id];
154 | }
155 |
156 |
--------------------------------------------------------------------------------
/src/starWarsDataResolver.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * These are types which correspond to the schema.
3 | * They represent the shape of the data visited during field resolution.
4 | */
5 | export interface Character {
6 | id: string;
7 | name: string;
8 | friends: ReadonlyArray;
9 | appearsIn: ReadonlyArray;
10 | }
11 |
12 | export interface Human {
13 | type: 'Human';
14 | id: string;
15 | name: string;
16 | friends: ReadonlyArray;
17 | appearsIn: ReadonlyArray;
18 | homePlanet?: string;
19 | }
20 |
21 | export interface Droid {
22 | type: 'Droid';
23 | id: string;
24 | name: string;
25 | friends: ReadonlyArray;
26 | appearsIn: ReadonlyArray;
27 | primaryFunction: string;
28 | }
29 |
30 | /**
31 | * This defines a basic set of data for our Star Wars Schema.
32 | *
33 | * This data is hard coded for the sake of the demo, but you could imagine
34 | * fetching this data from a backend service rather than from hardcoded
35 | * JSON objects in a more complex demo.
36 | */
37 |
38 | const luke: Human = {
39 | type: 'Human',
40 | id: '1000',
41 | name: 'Luke Skywalker',
42 | friends: ['1002', '1003', '2000', '2001'],
43 | appearsIn: [4, 5, 6],
44 | homePlanet: 'Tatooine',
45 | };
46 |
47 | const vader: Human = {
48 | type: 'Human',
49 | id: '1001',
50 | name: 'Darth Vader',
51 | friends: ['1004'],
52 | appearsIn: [4, 5, 6],
53 | homePlanet: 'Tatooine',
54 | };
55 |
56 | const han: Human = {
57 | type: 'Human',
58 | id: '1002',
59 | name: 'Han Solo',
60 | friends: ['1000', '1003', '2001'],
61 | appearsIn: [4, 5, 6],
62 | };
63 |
64 | const leia: Human = {
65 | type: 'Human',
66 | id: '1003',
67 | name: 'Leia Organa',
68 | friends: ['1000', '1002', '2000', '2001'],
69 | appearsIn: [4, 5, 6],
70 | homePlanet: 'Alderaan',
71 | };
72 |
73 | const tarkin: Human = {
74 | type: 'Human',
75 | id: '1004',
76 | name: 'Wilhuff Tarkin',
77 | friends: ['1001'],
78 | appearsIn: [4],
79 | };
80 |
81 | const humanData: { [id: string]: Human } = {
82 | [luke.id]: luke,
83 | [vader.id]: vader,
84 | [han.id]: han,
85 | [leia.id]: leia,
86 | [tarkin.id]: tarkin,
87 | };
88 |
89 | const threepio: Droid = {
90 | type: 'Droid',
91 | id: '2000',
92 | name: 'C-3PO',
93 | friends: ['1000', '1002', '1003', '2001'],
94 | appearsIn: [4, 5, 6],
95 | primaryFunction: 'Protocol',
96 | };
97 |
98 | const artoo: Droid = {
99 | type: 'Droid',
100 | id: '2001',
101 | name: 'R2-D2',
102 | friends: ['1000', '1002', '1003'],
103 | appearsIn: [4, 5, 6],
104 | primaryFunction: 'Astromech',
105 | };
106 |
107 | const droidData: { [id: string]: Droid } = {
108 | [threepio.id]: threepio,
109 | [artoo.id]: artoo,
110 | };
111 |
112 | /**
113 | * Helper function to get a character by ID.
114 | */
115 | function getCharacter(id: string): Promise {
116 | // Returning a promise just to illustrate that GraphQL.js supports it.
117 | return Promise.resolve(humanData[id] ?? droidData[id]);
118 | }
119 |
120 | /**
121 | * Allows us to query for a character's friends.
122 | */
123 | export function getFriends(
124 | character: Character,
125 | ): Array> {
126 | // Notice that GraphQL accepts Arrays of Promises.
127 | return character.friends.map((id) => getCharacter(id));
128 | }
129 |
130 | /**
131 | * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2.
132 | */
133 | export function getHero(episode: number): Character {
134 | if (episode === 5) {
135 | // Luke is the hero of Episode V.
136 | return luke;
137 | }
138 | // Artoo is the hero otherwise.
139 | return artoo;
140 | }
141 |
142 | /**
143 | * Allows us to query for the human with the given id.
144 | */
145 | export function getHuman(args: any): Human | null {
146 | const id = args.id
147 | return humanData[id];
148 | }
149 |
150 | /**
151 | * Allows us to query for the droid with the given id.
152 | */
153 | export function getDroid(id: string): Droid | null {
154 | return droidData[id];
155 | }
156 |
157 | export function updateHuman(args: any): Human | null {
158 | const id = args.id
159 | humanData[id][args.prop] = args.update;
160 | return humanData[id]
161 | }
--------------------------------------------------------------------------------
/ts.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions" : {
3 | "target" : "es5",
4 | "type" : ["node", "jest"],
5 | "module": "commonjs",
6 | "declaration" : "true",
7 | "strict": true,
8 | "moduleResolution" : "node"
9 | },
10 | "include": ["src"],
11 | "exclude": ["node_modules", "**/__tests__/*"]
12 | }
--------------------------------------------------------------------------------