├── .npmignore
├── tests.js
├── assets
├── QueryFlow-icon.png
├── QueryFlowTagLogo.png
├── QueryFlow-logo-white.png
└── exampleController.exampleFunction.png
├── .gitignore
├── package.json
├── LICENSE
├── exampleController.mjs
├── QueryFlow.js
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | # .npmignore
2 |
3 | tests.js
4 |
--------------------------------------------------------------------------------
/tests.js:
--------------------------------------------------------------------------------
1 | //Tests to verify query-flow-npm
2 |
3 |
--------------------------------------------------------------------------------
/assets/QueryFlow-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/query-flow-npm/HEAD/assets/QueryFlow-icon.png
--------------------------------------------------------------------------------
/assets/QueryFlowTagLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/query-flow-npm/HEAD/assets/QueryFlowTagLogo.png
--------------------------------------------------------------------------------
/assets/QueryFlow-logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/query-flow-npm/HEAD/assets/QueryFlow-logo-white.png
--------------------------------------------------------------------------------
/assets/exampleController.exampleFunction.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/query-flow-npm/HEAD/assets/exampleController.exampleFunction.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Dependency directories
39 | node_modules/
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "queryflow.js",
3 | "author": "Ryan Campbell, Philip Brown, Vivek Patel, George Greer, Niko Amescua",
4 | "version": "1.0.4",
5 | "description": "Lightweight library for automatically caching slow SQL queries within the backend middleware.",
6 | "main": "QueryFlow.js",
7 | "scoped": false,
8 | "publishConfig": {
9 | "organization": "@query-flow",
10 | "access": "public"
11 | },
12 | "dependencies": {
13 | "crypto-js": "^4.1.1"
14 | },
15 | "type": "module",
16 | "scripts": {
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/oslabs-beta/query-flow-npm.git"
22 | },
23 | "keywords": [
24 | "QueryFlow",
25 | "query-flow-npm",
26 | "query-flow",
27 | "queryflow",
28 | "SQL",
29 | "Redis",
30 | "cache",
31 | "memory"
32 | ],
33 | "license": "ISC",
34 | "homepage": "https://www.query-flow.com"
35 | }
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/exampleController.mjs:
--------------------------------------------------------------------------------
1 | import db from "../models/ourDBModel.mjs";
2 | import redisModel from "../models/redisModel.mjs";
3 | import QueryFlow from "queryflow.js";
4 |
5 | const exampleController = {};
6 |
7 | exampleController.example = async (req, res, next) => {
8 | const queryString = "SELECT * FROM users WHERE firstname = $1 AND lastname = $2";
9 | const { firstName, lastName } = req.body;
10 | const values = [firstName, lastName];
11 | const threshold = 3000; //Milliseconds
12 | const TTL = 30; //Seconds
13 | try {
14 | const result = await QueryFlow.autoCache({
15 | redisModel,
16 | db,
17 | queryString,
18 | values,
19 | threshold, //OPTIONAL: Default value 3 seconds.
20 | TTL, //OPTIONAL: Default value 30 minutes.
21 | log:true, //OPTIONAL: Default value false. Turn console logs on and off.
22 | instanceLatency:true //OPTIONAL: Default value false. Switches measurement of time from measuring total latency to measuring total time within the primary database itself.
23 | });
24 | res.locals.data = result.rows;
25 | return next();
26 | } catch (error) {
27 | return next({
28 | log: "Error handler caught error in middleware",
29 | status: 500,
30 | message: "Error handler caught error in middleware",
31 | });
32 | }
33 | };
34 |
35 | export default exampleController;
--------------------------------------------------------------------------------
/QueryFlow.js:
--------------------------------------------------------------------------------
1 | import CryptoJS from "crypto-js";
2 |
3 | const QueryFlow = {};
4 |
5 | QueryFlow.autoCache = async ({redisModel, db, queryString, values, threshold = 3000, TTL = 1800, log = false, instanceLatency = false}) => {
6 |
7 | //Check querystring is 'Read' Type. If not, throw new error.
8 | const stringCheck = async () => {
9 | if(!queryString.toLowerCase().startsWith('select')){
10 | console.error('Query string must be of read type only. I.E. SELECT');
11 | const string = {text: queryString, values: values};
12 | const result = await db(string);
13 | return result;
14 | }
15 | };
16 | stringCheck(queryString);
17 |
18 | const string = {text: queryString, values: values};
19 |
20 | //Create a unique key for redis storage
21 | let keyConcat = '';
22 | for (let i = 0; i < values.length; i++){
23 | keyConcat += values[i].toString()
24 | }
25 | keyConcat = keyConcat + queryString
26 |
27 | //Create the hash key for the data to be stored.
28 | const hash = CryptoJS.MD5(keyConcat).toString();
29 |
30 | //Attempt to retrieve data from Redis instance.
31 | const getResultRedis = await redisModel.json.get(hash, {
32 | path: '.',
33 | });
34 |
35 | //CACHE HIT
36 | if (getResultRedis){
37 | if (log) console.log(`Returned data associated with key ${hash} from Redis`)
38 | return getResultRedis;
39 |
40 | //CACHE MISS
41 | } else if (!instanceLatency) {
42 | const startTime = process.hrtime();
43 | const resultSQL = await db(string);
44 | const endTime = process.hrtime(startTime);
45 | const totalTimeHrTime = (endTime[0] * 1000 + endTime[1] / 1000000).toFixed(2);
46 | if (totalTimeHrTime > threshold) {
47 | const addToCache = async () => {
48 | try {
49 | if (log)console.log(`Set Redis with key ${hash} and data`);
50 | await redisModel.json.set(hash, '.', resultSQL);
51 | await redisModel.expire(hash, TTL);
52 | } catch (err) {
53 | console.error('Error setting JSON data in Redis Database:', err);
54 | }
55 | }
56 | addToCache();
57 | }
58 | return resultSQL;
59 | } else {
60 | const appendedString = 'EXPLAIN (ANALYZE true, COSTS true, SETTINGS true, BUFFERS true, WAL true, SUMMARY true, FORMAT JSON)' + `${queryString}`;
61 | const query = {text: appendedString, values: values};
62 | const data = await db(query)
63 | const totalTimeInstance = (Number(((data.rows[0]['QUERY PLAN'][0]['Planning Time']) + (data.rows[0]['QUERY PLAN'][0]['Execution Time']))));
64 | const resultSQL = await db(string);
65 | if (totalTimeInstance > threshold) {
66 | const addToCache = async () => {
67 | try {
68 | if (log) console.log(`Set Redis with key ${hash} and data`);
69 | await redisModel.json.set(hash, '.', resultSQL);
70 | await redisModel.expire(hash, TTL);
71 | } catch (err) {
72 | console.error('Error setting JSON data in Redis Database:', err);
73 | }
74 | }
75 | addToCache();
76 | }
77 | return resultSQL;
78 | }
79 | };
80 |
81 | export default QueryFlow;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # queryflow.js
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
14 | Lightweight middleware function that automatically caches SQL query results for increased performance.
15 |
16 |
17 |
18 | Analyze & Visualize »
19 |
20 |
21 | Report Bug
22 | ·
23 | Request Feature
24 |
58 |
59 |