UNPKG

rediqless

Version:

A tool to cache graphQL responses by leveraging Redis.

378 lines (321 loc) 14.2 kB
/** * * @description * The RediCache Class Object uses incoming queries as a map to navigate Redis' key-value store; destructuring the queries into a series of constant time-readable key-value pairs with references to connected nodes. If RediCache finds that incoming query information is stored within Redis, RediCache is able to return that requested data in sub-15ms time. If RediCache does not find the incoming query within the Redis Store, the query is sent back through RediQL to grab the information in the requested API schema via GraphQL. As the data returns to the client, it is simultaneously sent back to RediCache to store that data for future use. */ class RediCache { constructor(QLQueryObj, redisClient, QLResponse = {}, rawQuery) { // REQUESTED FIELDS THAT ARE NOT FOUND IN THE CACHE ARE PUSHED INTO THIS ARRAY this.redisFields = []; // INITIALLY THE RAW QUERY BEING MADE, THEN IT WILL BE REDEFINED AS AN OBJECT OF FIELDS/TYPES this.QLQueryObj = QLQueryObj; // WILL STORE THE NEXT NESTED QUERY TO RECURSIVELY TAKE PLACE OF QLQUERY OBJ this.nextType = []; // THE RESPONSE RECEIVED FROM GQL REQUEST this.QLResponse = QLResponse; // IF REDISFIELDS IS EMPTY, WE FORM A NEW RESPONSE FROM CACHE this.newResponse = {}; // THE ACTUAL REDIS CLIENT this.redisClient = redisClient; // INITIALIZED AS FALSE, IF FALSE, THE INFORMATION DOES NOT EXIST YET IN THE CACHE this.rediResponse = false; // AN ARRAY OF THE UNIQUE ID'S BASED ON FIELD 1 OF EACH TYPE this.keyIndex; this.exactQuery = false; this.rawQuery = rawQuery; } /// ASYNCRONOUS METHOD WHICH CHECKS TO SEE IF A KEYINDEX EXISTS async createQuery() { // INSTANTIATE ARGSID AND ARGS let argsId; let args; // this.rawQuery = JSON.stringify(this.QLQueryObj); this.exactQuery = await this.checkRedis(this.rawQuery); if (this.exactQuery > 0) { this.rediResponse = true; this.newResponse = JSON.parse(await this.getFromRedis(this.rawQuery)); return; } // USING REDIS FUNCTION TO SEE IF KEYINDEX EXISTS - IF IT DOESNT, ASSIGNE TO AN EMPTY ARRAY this.keyIndex = JSON.parse(await this.getFromRedis("keyIndex")) || []; // QUERY TYPES IS LOCATING THE FIRST TYPE IN THE QUERY const types = this.QLQueryObj["definitions"][0].selectionSet.selections[0].name.value; // ESTABLISHES AN ARRAY OF FIELD OBJECTS const fields = this.QLQueryObj["definitions"][0].selectionSet.selections[0].selectionSet .selections; // IF ARGUMENTS ID AND ARGUMENTS ARE DEFINED IN QUERY AST, ASSIGN THEM TO ARGSID AND ARGS if ( this.QLQueryObj["definitions"][0].selectionSet.selections[0] .arguments[0] !== undefined ) { argsId = this.QLQueryObj["definitions"][0].selectionSet.selections[0] .arguments[0].name.value; args = this.QLQueryObj["definitions"][0].selectionSet.selections[0] .arguments[0].value.value; } // INSTANTIATE NEXTTYPE let nextType; // CHECKS TO SEE IF THE NEXTTYPE IN THE QUERY IS NESTED (OR IF IT EXISTS) const fieldsArr = fields.map((field) => { // CHECKS TO SEE IF SELECTIONSET IS UNDEFINED (ARRAY OF FIELDS) IF IT IS, NEXTTYPE GETS REASSIGNED TO THE FILED OF FIELDSOBK if (field.selectionSet !== undefined) nextType = field; //RETURN FIELD.NAME.VALUE return field.name.value; }); // IF WE DISCOVERED A NESTED TYPE, THEN THIS.NEXTTYPE GETS PASED INTO NEXTED QUERY if (nextType) this.nextType = await this.nestedQuery(nextType); // redis fields will check the fields arr and return only fields that don't have existing keys in redis // REDIS FIELDS WILL CHECK // THIS IS GOING TO THE FIELDS ARRAY AND CHECKING REDIS WITH THE FIELDS-ID if (this.keyIndex) { for (let i = 0; i < fieldsArr.length; i++) { // IF FIELDSARR[i] IS NOT EQUAL TO NEXTTYPE, THEN WE CHECK FOR IT IN REDIS if (fieldsArr[i] !== this.nextType.types) { let exists; if (args) { exists = await this.checkRedis(`${fieldsArr[i]} ${args}`); } else { exists = await this.checkRedis( // **POTENTIAL OPTIMIZATION POINT [0] `${fieldsArr[i]} ${this.keyIndex[0]}` ); // IF IT DOESNT EXIST IN THE CACHE, PUSH IT TO THE FIELDSARR AND THE INDEX } if (!exists) { this.redisFields.push(fieldsArr[i]); break; } } } } // IF REDISFIELDS LENGTH IS ZERO, AND KEYINDEX IS TRUE - ALL THE VALUES IN QUERY WERE FOUND IN THE CACHE if (this.redisFields.length == 0 && this.keyIndex) this.rediResponse = true; // INSTANTIATE AS NULL let argsAndId = null; //IF ARGS IS UNDEFINED, DESTRUCTURE ARGS ID AND ARGS TOGETHER if (args !== undefined) argsAndId = [argsId, args]; this.QLQueryObj = { types: [types], fieldsArr: fieldsArr, arguments: argsAndId, // --> [argsId, args] }; // IF THE NEXTTYPE IS AN OBJ, SAVE IT TO REDIS if (typeof this.nextType == "object") this.redisClient.setex("nextType", 3600, JSON.stringify(this.nextType)); // IF ALL ALL VALUES WERE FOUND IN REDIS, INVOKE CREATE RESPONSE if (this.rediResponse) await this.createResponse(); } // <--- END OF CREATEQUERY ---> // // CREATE RESPONSE CREATES THE REDIS RESPONSE async createResponse() { // CREATE KEY/VALUE PAIR IN EMPTY NEWRESPONSE OBJ this.newResponse[`${this.QLQueryObj.types}`] = []; //IF A RESPONSE BY ID IS REQUESTED (BY WAY OF SINGLE VALUE REQUEST) if (this.QLQueryObj.arguments) { this.newResponse[`${this.QLQueryObj.types}`][0] = {}; //LOOPS THROUGH QL QUERY OBJ for (let i = 0; i < this.QLQueryObj.fieldsArr.length; i++) { // IF THE QLQUERYOBJ AT I HAS A NESTED TYPE... if (this.QLQueryObj.fieldsArr[i] === this.nextType.types) { // PROCESS THE NESTED RESPONSE this.newResponse[`${this.QLQueryObj.types}`][0][this.nextType.types] = {}; // CREATE NESTED RESPONSE VIA NESTEDCREATE METHOD await this.nestedCreate(0); } else { // NO NESTED TYPE DETECTED // REFERENCE KEY TO OBTAIN KEY FROM CACHE const keyReference = `${this.QLQueryObj.fieldsArr[i]}` + ` ${this.QLQueryObj.arguments[1]}`; // REDIRESPONSE SET TO OBTAINING THE INFO VIA KEYREFERENCE FROM REDIS let redisResponse = await this.getFromRedis(keyReference); // *TYPE CONVERSION* //convert number string into number type //isNaN checking to see if redisResponse is a number-string if (!isNaN(+redisResponse)) redisResponse = Number(redisResponse); //convert boolean strings to booleans if (redisResponse === "true") redisResponse = true; if (redisResponse === "false") redisResponse = false; this.newResponse[`${this.QLQueryObj.types}`][0][ `${this.QLQueryObj.fieldsArr[i]}` ] = redisResponse; } } // ELSE: IF NO ARGUMENTS WERE DETECTED } else { //STORE ARRAY OF IDS IN REDIS WITH KEY OF KEYINDEX IN CACHERESP this.keyIndex = JSON.parse(await this.getFromRedis("keyIndex")); // LOOP THROUGH KEYINDEX (AMOUNT OF DATA FROM INITIAL RESP IN REDIS) for (let j = 0; j < this.keyIndex.length; j++) { // J IS THE AMMT OF RESPONSES this.newResponse[`${this.QLQueryObj.types}`][j] = {}; //loops through graphQL response, caching the values as "`${fieldname + id}` : value". In this case the id is flight_number. for (let i = 0; i < this.QLQueryObj.fieldsArr.length; i++) { // I IS EACH INDIVIDUAL PIECE OF DATA // IF THE NEXT TYPE IS TYPES, WE ARE NESTED - PERFORM LOGIC if (this.QLQueryObj.fieldsArr[i] === this.nextType.types) { this.newResponse[`${this.QLQueryObj.types}`][j][ this.nextType.types ] = {}; await this.nestedCreate(j); } else { // <---- NEED TO WORK ON MAKING THIS CODE DRY AS IT LEVERAGED A FEW TIMES ----> // // WE ARE NOT NESTED const keyReference = `${this.QLQueryObj.fieldsArr[i]}` + ` ${this.keyIndex[j]}`; let redisResponse = await this.getFromRedis(keyReference); //convert number string into number type //isNaN checking to see if redisResponse is a number-string if (!isNaN(+redisResponse)) redisResponse = Number(redisResponse); //convert boolean strings to booleans if (redisResponse === "true") redisResponse = true; if (redisResponse === "false") redisResponse = false; this.newResponse[`${this.QLQueryObj.types}`][j][ `${this.QLQueryObj.fieldsArr[i]}` ] = redisResponse; } } } } this.redisClient.setex( this.rawQuery, 3600, JSON.stringify(this.newResponse) ); } // <----------------------------------------------> // LOGIC INVOKES NESTEDQUERY IF WE HAVE A NESTED QUERY async nestedQuery(field) { const queryType = field.name.value; const fields = field.selectionSet.selections; const fieldsArr = fields.map((field) => { if (field.selectionSet !== undefined) { // this.nestedQuery(field) this.nextType = this.nestedQuery(field); } return field.name.value; }); // REDIS FIELDS WILL CHECK THE FIELDS ARR AND RETURN ONLY FIELDS THAT DON'T HAVE EXISTING KEYS IN REDIS for (let i = 0; i < fieldsArr.length; i++) { let exists = await this.checkRedis(`${fieldsArr[i]} ${this.keyIndex[0]}`); if (!exists) { this.redisFields.push(fieldsArr[i]); break; } } const QLQueryObj = { types: queryType, fieldsArr: fieldsArr, }; return QLQueryObj; } async nestedCreate(j) { for (let i = 0; i < this.nextType.fieldsArr.length; i++) { const keyReference = `${this.nextType.fieldsArr[i]}` + ` ${this.keyIndex[j]}`; let redisResponse = await this.getFromRedis(keyReference); //convert number string into number type //isNaN checking to see if redisResponse is a number-string if (!isNaN(+redisResponse)) redisResponse = Number(redisResponse); //convert boolean strings to booleans if (redisResponse === "true") redisResponse = true; if (redisResponse === "false") redisResponse = false; this.newResponse[`${this.QLQueryObj.types}`][j][this.nextType.types][ this.nextType.fieldsArr[i] ] = redisResponse; } } // CACHERESPONSE: TAKES DATE FROM THE API REPONSE (AFTER QUERY) AND SAVES TO REDIS async cacheResponse() { this.keyIndex = []; //send request from createQuery //keyExists checks Redis for the given key let keyExists = false; //given a unique value (in the demo it is flight_number) //the corresponding values could be cached with that unique value concatenated for the key value. this.newResponse[`${this.QLQueryObj.types}`] = []; //j will iterate through array of objects in response. In this case each launch. for (let j = 0; j < this.QLResponse[this.QLQueryObj.types].length; j++) { //push key into array for later reference. this.keyIndex.push( this.QLResponse[this.QLQueryObj.types][j][this.QLQueryObj.fieldsArr[0]] ); this.newResponse[`${this.QLQueryObj.types}`][j] = {}; //loops through graphQL response, caching the values as "`${fieldname + id}` : value". In this case the id is flight_number. for (let i = 0; i < this.QLQueryObj.fieldsArr.length; i++) { if (this.QLQueryObj.fieldsArr[i] === this.nextType.types) { await this.nestedResponse( this.QLResponse[this.QLQueryObj.types[0]][j][ this.QLQueryObj.fieldsArr[i] ], j, i ); continue; } const redisKey = `${this.QLQueryObj.fieldsArr[i]}` + ` ${ this.QLResponse[this.QLQueryObj.types][j][ this.QLQueryObj.fieldsArr[0] ] }`; const value = `${ this.QLResponse[this.QLQueryObj.types][j][ this.QLQueryObj.fieldsArr[i] ] }`; this.redisClient.setex(redisKey, 3600, value); } //save keyIndex array into redis this.redisClient.setex("keyIndex", 3600, JSON.stringify(this.keyIndex)); this.redisClient.setex( this.rawQuery, 3600, JSON.stringify(this.QLResponse) ); } } // if cacheResponse finds nested object nestedResponse caches those nested values async nestedResponse(nestedResponse, j, h) { //loops through graphQL response, caching the values as "`${fieldname + id}` : value". In this case the id is flight_number. for (let i = 0; i < this.nextType.fieldsArr.length; i++) { const redisKey = `${this.nextType.fieldsArr[i]}` + ` ${ this.QLResponse[this.QLQueryObj.types][j][ this.QLQueryObj.fieldsArr[0] ] }`; const value = `${nestedResponse[this.nextType.fieldsArr[i]]}`; this.redisClient.setex(redisKey, 3600, value); } } checkRedis(key) { return new Promise((resolve, reject) => { this.redisClient.exists(key, (err, exists) => { err ? reject(err) : resolve(exists); }); }); } getFromRedis(key) { const redisResponse = new Promise((resolve, reject) => { this.redisClient.get(key, (error, result) => error ? reject(error) : resolve(result) ); }); //convert number string into number type //isNaN checking to see if redisResponse is a number-string if (!isNaN(+redisResponse)) redisResponse = Number(redisResponse); //convert boolean strings to booleans if (redisResponse === "true") redisResponse = true; if (redisResponse === "false") redisResponse = false; return redisResponse; } } module.exports = RediCache;