├── .gitignore ├── package.json ├── readme.md └── src ├── __tests__ └── start.test..js ├── constants.js ├── db ├── select_db.js └── update_db.js ├── execute.js ├── helpers ├── date_helper.js └── string_helper.js ├── index.js ├── models └── fbSqlQuery.js ├── parser ├── __tests__ │ ├── query_parser.test.js │ └── query_parser_int.test.js └── query_parser.js └── query_runners ├── __tests__ ├── firestore │ ├── delete_firestore_int.test.js │ ├── insert_firestore_int.test.js │ ├── select_firestore_int.test.js │ └── update_firestore_int.test.js └── realtime │ ├── delete_realtime_int.test.js │ ├── insert_realtime_int.test.js │ ├── select_realtime_int.test.js │ └── update_realtime_int.test.js ├── delete.js ├── insert.js ├── select.js ├── test_resources ├── db_config.js └── setup_db.js └── update.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | dist 4 | 5 | *db_config.js 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fbsql", 3 | "version": "1.0.12", 4 | "license": "ISC", 5 | "author": "mrjoeroddy@gmail.com", 6 | "description": "Exectues SQL queries against firebase databases", 7 | "scripts": { 8 | "build": "rm -rf dist/ && babel -d dist/ src/", 9 | "start": "npm run build && node dist/index.js", 10 | "test": "jest --runInBand" 11 | }, 12 | "main": "dist/index.js", 13 | "files": [ 14 | "**/dist/", 15 | "**/src/", 16 | "**/package.json" 17 | ], 18 | "keywords": [ 19 | "firebase", 20 | "sql", 21 | "javascript" 22 | ], 23 | "dependencies": { 24 | "@babel/polyfill": "^7.2.5", 25 | "@babel/runtime": "^7.2.0", 26 | "babel-core": "^7.0.0-0", 27 | "firebase": "^5.8.0", 28 | "firebase-admin": "^6.5.0", 29 | "lodash": "^4.17.11", 30 | "moment": "^2.23.0", 31 | "npm": "^6.7.0" 32 | }, 33 | "devDependencies": { 34 | "babel-jest": "^23.6.0", 35 | "@babel/cli": "^7.0.0", 36 | "@babel/core": "^7.2.2", 37 | "@babel/plugin-proposal-class-properties": "^7.2.3", 38 | "@babel/plugin-transform-runtime": "^7.2.0", 39 | "@babel/preset-env": "^7.2.3", 40 | "jest": "^23.6.0" 41 | }, 42 | "babel": { 43 | "presets": [ 44 | "@babel/preset-env" 45 | ], 46 | "plugins": [ 47 | "@babel/plugin-transform-runtime", 48 | "@babel/plugin-proposal-class-properties" 49 | ] 50 | }, 51 | "jest": { 52 | "verbose": false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Firebase SQL 2 | 3 | The Firebase SQL library accepts standard SQL queries and executes them as corresponding Firebase queries. 4 | 5 | ```javascript 6 | import fbsql from "fbsql"; 7 | 8 | // async: 9 | const codingBlogs = await fbsql(`select * from blogs where genre = "coding";`); 10 | 11 | // or apply a listener: 12 | fbsql(`select * from users where online = true;`, onlineUsers => { 13 | // handle realtime updates... 14 | }); 15 | ``` 16 | 17 | ## Installation 18 | 19 | ```bash 20 | npm install --save fbsql 21 | ``` 22 | 23 | ## Wait, but why? 24 | 25 | Fbsql was extracted from the [Firestation desktop app's](https://github.com/JoeRoddy/firestation/) source code to make issue tracking easier. 26 | 27 | This library may be useful for: 28 | 29 | - Developers who prefer SQL syntax to the Firebase API 30 | - Saving time writing complicated functions that could be achieved with a one line SQL statement 31 | - Improving code clarity: 32 | 33 | ```js 34 | // firebase-sql 35 | const codingBlogs = await fbsql(`select * from blogs where genre = "coding";`); 36 | 37 | // realtime db 38 | const snapshot = await firebase 39 | .database() 40 | .ref("/blogs/") 41 | .orderByChild("genre") 42 | .equalTo("coding") 43 | .once("value"); 44 | const codingBlogs = snapshot.val(); 45 | 46 | // firestore 47 | const doc = await firebase 48 | .firestore() 49 | .collection("blogs") 50 | .where("genre", "==", "coding") 51 | .get(); 52 | const codingBlogs = doc.data(); 53 | ``` 54 | 55 | ## Setup 56 | 57 | Wherever you initialize firebase: 58 | 59 | ```js 60 | import firebase from "firebase/app"; // import * as firebase from "firebase-admin"; 61 | import { configureFbsql } from "fbsql"; 62 | 63 | firebase.initializeApp({ your config... }); 64 | configureFbsql({ app: firebase }); 65 | ``` 66 | 67 | If you run into errors saying `app.database() is not a function`, you may need to import firebase into the file causing the issue. `import firebase from "firebase/app";` 68 | 69 | ## Configuration 70 | 71 | You have multiple configuration options through the `configureFbsql` function: 72 | 73 | ```javascript 74 | import fbsql, { configureFbsql } from "fbsql"; 75 | 76 | // pass any combination of options 77 | // below are the defaults 78 | configureFbsql({ 79 | app: null // your firebase app 80 | isFirestore: false, // use firestore instead of the realtime db? 81 | shouldCommitResults: true, // commit changes on inserts, updates, deletes? 82 | shouldExpandResults: false // return a more detailed res obj from queries? 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /src/__tests__/start.test..js: -------------------------------------------------------------------------------- 1 | import fbsql, { configureFbsql, getConfig } from "../"; 2 | 3 | test("imports working", () => { 4 | expect(typeof fbsql).toBe("function"); 5 | expect(typeof configureFbsql).toBe("function"); 6 | expect(typeof getConfig()).toBe("object"); 7 | }); 8 | 9 | test("configure working", () => { 10 | configureFbsql({ 11 | isFirestore: true, 12 | shouldCommitResults: true, 13 | shouldExpandResults: false 14 | }); 15 | const { isFirestore, shouldCommitResults, shouldExpandResults } = getConfig(); 16 | expect(isFirestore).toBeTruthy(); 17 | expect(shouldCommitResults).toBeTruthy(); 18 | expect(shouldExpandResults).toBeFalsy(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const SELECT_STATEMENT = "SELECT_STATEMENT"; 2 | export const UPDATE_STATEMENT = "UPDATE_STATEMENT"; 3 | export const INSERT_STATEMENT = "INSERT_STATEMENT"; 4 | export const DELETE_STATEMENT = "DELETE_STATEMENT"; 5 | export const FIRESTATION_DATA_PROP = "FIRESTATION_DATA_PROP"; 6 | export const NO_EQUALITY_STATEMENTS = "NO_EQUALITY_STATEMENTS"; 7 | export const EQUATION_IDENTIFIERS = [" / ", " + ", " - ", " * "]; 8 | -------------------------------------------------------------------------------- /src/db/select_db.js: -------------------------------------------------------------------------------- 1 | import { getApp } from "../index"; 2 | import stringHelper from "../helpers/string_helper"; 3 | import { isValidDate, executeDateComparison } from "../helpers/date_helper"; 4 | import QueryDetails from "../models/fbSqlQuery"; 5 | 6 | const getDataForSelectAsync = query => { 7 | query.shouldApplyListener = false; 8 | return new Promise((resolve, reject) => { 9 | getDataForSelect(query, res => { 10 | resolve(res); 11 | }); 12 | }); 13 | }; 14 | 15 | const getDataForSelect = function(query, callback) { 16 | const { wheres, selectedFields, isFirestore } = query; 17 | const app = getApp(); 18 | let db = isFirestore ? app.firestore() : app.database(); 19 | //TODO: reimplement listeners, using firestore listeners as well 20 | let results = { 21 | statementType: "SELECT_STATEMENT", 22 | path: query.collection, 23 | orderBys: query.orderBys, 24 | payload: {}, 25 | isFirestore 26 | }; 27 | if ( 28 | !wheres || 29 | (wheres[0] && wheres[0] && wheres[0].error === "NO_EQUALITY_STATEMENTS") 30 | ) { 31 | //unfilterable query, grab whole collection 32 | const collectionCallback = res => { 33 | if (wheres && wheres[0]) { 34 | res.payload = filterWheresAndNonSelectedFields( 35 | res.payload, 36 | wheres, 37 | selectedFields 38 | ); 39 | // results.firebaseListener = ref; 40 | } 41 | return callback(res); 42 | }; 43 | query.isFirestore 44 | ? unfilteredFirestoreQuery(db, results, query, collectionCallback) 45 | : queryEntireRealtimeCollection(db, results, query, collectionCallback); 46 | } else { 47 | //filterable query 48 | query.isFirestore 49 | ? executeFilteredFirestoreQuery(db, results, query, callback) 50 | : executeFilteredRealtimeQuery(db, results, query, callback); 51 | } 52 | }; 53 | 54 | const unfilteredFirestoreQuery = function(db, results, query, callback) { 55 | const { collection, selectedFields, shouldApplyListener } = query; 56 | if (collection === "/") { 57 | //root query: select * from /; 58 | // TODO: Listener 59 | db.getCollections() 60 | .then(collections => { 61 | if (collections.length === 0) { 62 | // no collections 63 | results.payload = null; 64 | return callback(results); 65 | } 66 | let numDone = 0; 67 | let firestoreData = {}; 68 | collections.forEach(collection => { 69 | let colId = collection.id; 70 | let query = new QueryDetails(); 71 | query.collection = colId; 72 | unfilteredFirestoreQuery(db, { payload: {} }, query, res => { 73 | firestoreData[colId] = res.payload; 74 | if (++numDone >= collections.length) { 75 | results.payload = firestoreData; 76 | return callback(results); 77 | } 78 | }); 79 | }); 80 | }) 81 | .catch(err => { 82 | console.log("Err getting cols:", err); 83 | results.error = err.message; 84 | return callback(results); 85 | }); 86 | } else if (collection.includes("/")) { 87 | let [col, docId, ...propPath] = collection.split(`/`); 88 | // users.userId.age => col: users, docId:userId, propPath: [age] 89 | docId = stringHelper.replaceAll(docId, "/", "."); 90 | const ref = db.collection(col).doc(docId); 91 | const fetchData = shouldApplyListener 92 | ? listenToFirestoreDoc 93 | : getFirstoreDocOnce; 94 | 95 | fetchData( 96 | ref, 97 | ({ docData, unsub }) => { 98 | if (!docData) { 99 | results.error = { message: "No such document" }; 100 | return callback(results); 101 | } 102 | results.payload = 103 | propPath.length > 0 ? getDataAtPropPath(docData, propPath) : docData; 104 | results = removeFieldsAndApplyUnsub(results, selectedFields, { 105 | type: "firestore", 106 | unsub 107 | }); 108 | 109 | return callback(results); 110 | }, 111 | err => { 112 | results.error = { message: "No such document" }; 113 | return callback(results); 114 | } 115 | ); 116 | } else { 117 | //select * from collection 118 | const fetchData = shouldApplyListener 119 | ? listenToFirestoreCol 120 | : getFirstoreColOnce; 121 | 122 | fetchData( 123 | db.collection(collection), 124 | ({ data, unsub }) => { 125 | results.payload = data; 126 | results = removeFieldsAndApplyUnsub(results, selectedFields, { 127 | type: "firestore", 128 | unsub 129 | }); 130 | return callback(results); 131 | }, 132 | err => { 133 | results.error = err.message; 134 | return callback(results); 135 | } 136 | ); 137 | } 138 | }; 139 | 140 | const removeFieldsAndApplyUnsub = ( 141 | results, 142 | selectedFields, 143 | { type, unsub } 144 | ) => { 145 | if (selectedFields) { 146 | results.payload = removeNonSelectedFieldsFromResults( 147 | results.payload, 148 | selectedFields 149 | ); 150 | } 151 | if (type && unsub) { 152 | results.firebaseListener = { 153 | type, 154 | unsubscribe: () => unsub() 155 | }; 156 | } 157 | return results; 158 | }; 159 | 160 | const getDataAtPropPath = (data, propPath = []) => { 161 | let propData; 162 | propPath.forEach(prop => { 163 | let subData = data[prop]; 164 | if (!subData) return null; 165 | propData = subData; 166 | }); 167 | return propData; 168 | }; 169 | 170 | const getFirstoreDocOnce = (ref, callback, onErr) => { 171 | ref 172 | .get() 173 | .then(doc => callback({ docData: doc.exists ? doc.data() : null })) 174 | .catch(onErr); 175 | }; 176 | 177 | const listenToFirestoreDoc = (ref, callback, onErr) => { 178 | let unsub = ref.onSnapshot(doc => 179 | callback({ docData: doc.exists ? doc.data() : null, unsub }, onErr) 180 | ); 181 | }; 182 | 183 | const getFirstoreColOnce = (ref, callback, onErr) => { 184 | ref 185 | .get() 186 | .then(snapshot => { 187 | let data = {}; 188 | snapshot.forEach(doc => { 189 | data[doc.id] = doc.data(); 190 | }); 191 | return callback({ data }); 192 | }) 193 | .catch(onErr); 194 | }; 195 | 196 | const listenToFirestoreCol = (ref, callback, onErr) => { 197 | let unsub = ref.onSnapshot(snapshot => { 198 | let data = {}; 199 | snapshot.forEach(doc => { 200 | data[doc.id] = doc.data(); 201 | }); 202 | return callback({ data, unsub }); 203 | }, onErr); 204 | }; 205 | 206 | const queryEntireRealtimeCollection = function(db, results, query, callback) { 207 | const { collection, selectedFields, shouldApplyListener } = query; 208 | const ref = db.ref(collection); 209 | const queryCallback = snapshot => { 210 | results.payload = snapshot.val(); 211 | if (selectedFields) { 212 | results.payload = removeNonSelectedFieldsFromResults( 213 | results.payload, 214 | selectedFields 215 | ); 216 | } 217 | results.firebaseListener = shouldApplyListener 218 | ? { 219 | unsubscribe: () => ref.off("value"), 220 | type: "realtime" 221 | } 222 | : null; 223 | return callback(results); 224 | }; 225 | 226 | shouldApplyListener 227 | ? ref.on("value", queryCallback) 228 | : ref.once("value").then(queryCallback); 229 | }; 230 | 231 | const executeFilteredFirestoreQuery = function(db, results, query, callback) { 232 | const { collection, selectedFields, wheres, shouldApplyListener } = query; 233 | const mainWhere = wheres[0]; 234 | // TODO: where chaining if we have multiple filterable, rather than client side filter 235 | // ie: citiesRef.where("state", "==", "CO").where("name", "==", "Denver") 236 | // TODO: promise version 237 | let unsub = db 238 | .collection(collection) 239 | .where(mainWhere.field, mainWhere.comparator, mainWhere.value) 240 | .onSnapshot( 241 | snapshot => { 242 | let payload = {}; 243 | snapshot.forEach(doc => { 244 | payload[doc.id] = doc.data(); 245 | }); 246 | payload = filterWheresAndNonSelectedFields( 247 | payload, 248 | wheres, 249 | selectedFields 250 | ); 251 | results.payload = payload; 252 | results.firebaseListener = { 253 | type: "firestore", 254 | unsubscribe: () => unsub() 255 | }; 256 | callback(results); 257 | }, 258 | err => { 259 | results.error = err.message; 260 | return callback(results); 261 | } 262 | ); 263 | }; 264 | 265 | const executeFilteredRealtimeQuery = function(db, results, query, callback) { 266 | const { collection, selectedFields, wheres, shouldApplyListener } = query; 267 | const mainWhere = wheres[0]; 268 | const ref = db 269 | .ref(collection) 270 | .orderByChild(mainWhere.field) 271 | .equalTo(mainWhere.value); 272 | 273 | const resCallback = snapshot => { 274 | results.payload = filterWheresAndNonSelectedFields( 275 | snapshot.val(), 276 | wheres, 277 | selectedFields 278 | ); 279 | results.firebaseListener = shouldApplyListener 280 | ? { 281 | unsubscribe: () => ref.off("value"), 282 | type: "realtime" 283 | } 284 | : null; 285 | return callback(results); 286 | }; 287 | 288 | shouldApplyListener 289 | ? ref.on("value", resCallback) 290 | : ref.once("value").then(resCallback); 291 | }; 292 | 293 | const filterWheresAndNonSelectedFields = function( 294 | resultsPayload, 295 | wheres, 296 | selectedFields 297 | ) { 298 | if (wheres.length > 1) { 299 | resultsPayload = filterResultsByWhereStatements( 300 | resultsPayload, 301 | wheres.slice(1) 302 | ); 303 | } 304 | if (selectedFields) { 305 | resultsPayload = removeNonSelectedFieldsFromResults( 306 | resultsPayload, 307 | selectedFields 308 | ); 309 | } 310 | return resultsPayload; 311 | }; 312 | 313 | const removeNonSelectedFieldsFromResults = (results, selectedFields) => { 314 | if (!results || !selectedFields) { 315 | return results; 316 | } 317 | Object.keys(results).forEach(objKey => { 318 | if (typeof results[objKey] !== "object") { 319 | if (!selectedFields[objKey]) { 320 | delete results[objKey]; 321 | } 322 | } else { 323 | Object.keys(results[objKey]).forEach(propKey => { 324 | if (!selectedFields[propKey]) { 325 | delete results[objKey][propKey]; 326 | } 327 | }); 328 | } 329 | }); 330 | return Object.keys(results).length === 1 331 | ? results[Object.keys(results)[0]] 332 | : results; 333 | }; 334 | 335 | const filterResultsByWhereStatements = (results, whereStatements) => { 336 | if (!results) { 337 | return null; 338 | } 339 | let returnedResults = {}; 340 | let nonMatch = {}; 341 | for (let i = 0; i < whereStatements.length; i++) { 342 | let where = whereStatements[i]; 343 | Object.keys(results).forEach(key => { 344 | let thisResult = results[key][where.field]; 345 | if (!conditionIsTrue(thisResult, where.value, where.comparator)) { 346 | nonMatch[key] = results[key]; 347 | } 348 | }); 349 | } 350 | if (nonMatch) { 351 | Object.keys(results).forEach(key => { 352 | if (!nonMatch[key]) { 353 | returnedResults[key] = results[key]; 354 | } 355 | }); 356 | return returnedResults; 357 | } else { 358 | return results; 359 | } 360 | }; 361 | 362 | const conditionIsTrue = (val1, val2, comparator) => { 363 | switch (comparator) { 364 | case "==": 365 | return determineEquals(val1, val2); 366 | case "!=": 367 | return !determineEquals(val1, val2); 368 | case "<=": 369 | case "<": 370 | case ">=": 371 | case ">": 372 | return determineGreaterOrLess(val1, val2, comparator); 373 | case "like": 374 | return stringHelper.determineStringIsLike(val1, val2); 375 | case "!like": 376 | return !stringHelper.determineStringIsLike(val1, val2); 377 | default: 378 | throw "Unrecognized comparator: " + comparator; 379 | } 380 | }; 381 | 382 | const determineEquals = (val1, val2) => { 383 | val1 = typeof val1 == "undefined" || val1 == "null" ? null : val1; 384 | val2 = typeof val2 == "undefined" || val2 == "null" ? null : val2; 385 | return val1 === val2; 386 | }; 387 | 388 | const determineGreaterOrLess = (val1, val2, comparator) => { 389 | let isNum = false; 390 | if (isNaN(val1) || isNaN(val2)) { 391 | if (isValidDate(val1) && isValidDate(val2)) { 392 | return executeDateComparison(val1, val2, comparator); 393 | } 394 | } else { 395 | isNum = true; 396 | } 397 | switch (comparator) { 398 | case "<=": 399 | return isNum ? val1 <= val2 : val1.length <= val2.length; 400 | case ">=": 401 | return isNum ? val1 >= val2 : val1.length >= val2.length; 402 | case ">": 403 | return isNum ? val1 > val2 : val1.length < val2.length; 404 | case "<": 405 | return isNum ? val1 < val2 : val1.length < val2.length; 406 | } 407 | }; 408 | 409 | export { getDataForSelect, getDataForSelectAsync, unfilteredFirestoreQuery }; 410 | -------------------------------------------------------------------------------- /src/db/update_db.js: -------------------------------------------------------------------------------- 1 | import stringHelper from "../helpers/string_helper"; 2 | import { getApp } from "../index"; 3 | 4 | const updateFields = function(path, object, fields, isFirestore) { 5 | if (!fields || !object) { 6 | return; 7 | } 8 | // const app = startFirebaseApp(savedDatabase); 9 | const app = getApp(); 10 | return isFirestore 11 | ? updateFirestoreFields(app.firestore(), path, object, fields) 12 | : updateRealtimeFields(app.database(), path, object, fields); 13 | }; 14 | 15 | const updateRealtimeFields = function(db, path, newData, fields) { 16 | let updateObject = {}; 17 | fields.forEach(field => { 18 | updateObject[field] = newData[field]; 19 | }); 20 | 21 | return db.ref(path).update(updateObject); 22 | }; 23 | 24 | const updateFirestoreFields = function(db, path, object, fields) { 25 | let [col, doc] = path.split(/\/(.+)/); // splits only on first '/' char 26 | 27 | return db 28 | .collection(col) 29 | .doc(doc) 30 | .set(object); 31 | }; 32 | 33 | const deleteObject = function(path, isFirestore) { 34 | const app = getApp(); 35 | return isFirestore 36 | ? deleteFirestoreData(app.firestore(), path) 37 | : app 38 | .database() 39 | .ref(path) 40 | .remove(); 41 | }; 42 | 43 | const deleteFirestoreData = function(db, path) { 44 | let [collection, doc] = path.split(/\/(.+)/); //splits on first "/" 45 | return doc.includes("/") 46 | ? deleteFirestoreField(db, collection, doc) 47 | : deleteFirestoreDoc(db, collection, doc); 48 | }; 49 | 50 | const deleteFirestoreDoc = function(db, collection, doc) { 51 | return db 52 | .collection(collection) 53 | .doc(doc) 54 | .delete(); 55 | }; 56 | 57 | const deleteFirestoreField = function(db, collection, docAndField) { 58 | let [doc, field] = docAndField.split(/\/(.+)/); 59 | field = stringHelper.replaceAll(field, "/", "."); 60 | return db 61 | .collection(collection) 62 | .doc(doc) 63 | .update({ 64 | [field]: getApp().firestore.FieldValue.delete() 65 | }); 66 | }; 67 | 68 | const pushObject = function(path, object, isFirestore) { 69 | const app = getApp(); 70 | return isFirestore 71 | ? createFirestoreDocument(app.firestore(), path, object) 72 | : app 73 | .database() 74 | .ref(path) 75 | .push(object); 76 | }; 77 | 78 | const createFirestoreDocument = function(db, path, data) { 79 | let [collection, docId] = path.split(/\/(.+)/); 80 | return docId 81 | ? setFirestoreDocWithExplicitId(db, collection, docId, data) 82 | : pushFirestoreDocToGeneratedId(db, collection, data); 83 | }; 84 | 85 | const setFirestoreDocWithExplicitId = function(db, collection, docId, data) { 86 | return db 87 | .collection(collection) 88 | .doc(docId) 89 | .set(data); 90 | }; 91 | 92 | const pushFirestoreDocToGeneratedId = function(db, collection, data) { 93 | collection = collection.replace(/\/+$/, ""); //remove trailing "/" 94 | return db.collection(collection).add(data); 95 | }; 96 | 97 | const set = function(savedDatabase, path, data, isFirestore) { 98 | const app = getApp(); 99 | 100 | const db = isFirestore ? app.firestore() : app.database(); 101 | if (isFirestore) { 102 | let [collection, docId] = path.split(/\/(.+)/); 103 | docId.includes("/") 104 | ? setFirestoreProp(db, path, data) 105 | : setFirestoreDocWithExplicitId(db, collection, docId, data); 106 | } else { 107 | db.ref(path).set(data); 108 | } 109 | }; 110 | 111 | const setObjectProperty = function(savedDatabase, path, value, isFirestore) { 112 | const app = getApp(); 113 | value = stringHelper.getParsedValue(value); 114 | isFirestore 115 | ? setFirestoreProp(app.firestore(), path, value) 116 | : app 117 | .database() 118 | .ref(path) 119 | .set(value); 120 | }; 121 | 122 | const setFirestoreProp = function(db, path, value) { 123 | path = path.charAt(0) === "/" && path.length > 1 ? path.substring(1) : path; 124 | path = stringHelper.replaceAll(path, "/", "."); 125 | let [collection, docAndField] = path.split(/\.(.+)/); 126 | let [docId, field] = docAndField.split(/\.(.+)/); 127 | if (!field) { 128 | //trying to create a new doc from obj tree 129 | return createFirestoreDocument(db, collection, { [docId]: value }); 130 | } 131 | db.collection(collection) 132 | .doc(docId) 133 | .update({ 134 | [field]: value 135 | }); 136 | }; 137 | 138 | export { deleteObject, set, setObjectProperty, pushObject, updateFields }; 139 | -------------------------------------------------------------------------------- /src/execute.js: -------------------------------------------------------------------------------- 1 | import queryParser from "./parser/query_parser"; 2 | import executeSelect from "./query_runners/select"; 3 | import executeDelete from "./query_runners/delete"; 4 | import executeUpdate from "./query_runners/update"; 5 | import executeInsert from "./query_runners/insert"; 6 | 7 | import { 8 | SELECT_STATEMENT, 9 | UPDATE_STATEMENT, 10 | INSERT_STATEMENT, 11 | DELETE_STATEMENT 12 | } from "./constants"; 13 | 14 | export default function executeQuery(query, callback, shouldApplyListener) { 15 | query = queryParser.formatAndCleanQuery(query); 16 | const statementType = queryParser.determineStatementType(query); 17 | 18 | switch (statementType) { 19 | case SELECT_STATEMENT: 20 | return executeSelect(query, callback, shouldApplyListener); 21 | case UPDATE_STATEMENT: 22 | return executeUpdate(query, callback); 23 | case DELETE_STATEMENT: 24 | return executeDelete(query, callback); 25 | case INSERT_STATEMENT: 26 | return executeInsert(query, callback); 27 | default: 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers/date_helper.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export function formatDate(dateString) { 4 | let date = new Date(dateString); 5 | var monthNames = [ 6 | "JAN", 7 | "FEB", 8 | "MAR", 9 | "APR", 10 | "MAY", 11 | "JUN", 12 | "JUL", 13 | "AUG", 14 | "SEP", 15 | "OCT", 16 | "NOV", 17 | "DEC" 18 | ]; 19 | 20 | var day = date.getDate(); 21 | var monthIndex = date.getMonth(); 22 | var year = date.getFullYear(); 23 | 24 | return day + "-" + monthNames[monthIndex] + "-" + year; 25 | } 26 | 27 | export function isValidDate(dateString) { 28 | return moment(dateString).isValid; 29 | } 30 | 31 | export function executeDateComparison(val1, val2, comparator) { 32 | let m1 = moment(val1); 33 | let m2 = moment(val2); 34 | let diff = m1.diff(m2); 35 | switch (comparator) { 36 | case "<=": 37 | return diff <= 0; 38 | case ">=": 39 | return diff >= 0; 40 | case ">": 41 | return diff > 0; 42 | case "<": 43 | return diff < 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/helpers/string_helper.js: -------------------------------------------------------------------------------- 1 | class StringHelper { 2 | regexIndexOf(string, regex, startpos) { 3 | var indexOf = string.substring(startpos || 0).search(regex); 4 | return indexOf >= 0 ? indexOf + (startpos || 0) : indexOf; 5 | } 6 | 7 | replaceAll(string, regex, replacement) { 8 | return string.replace(new RegExp(regex, "g"), replacement); 9 | } 10 | 11 | replaceAllIgnoreCase(string, regex, replacement) { 12 | return string.replace(new RegExp(regex, "g", "i"), replacement); 13 | } 14 | 15 | regexLastIndexOf(string, regex, startpos) { 16 | regex = regex.global 17 | ? regex 18 | : new RegExp( 19 | regex.source, 20 | "g" + (regex.ignoreCase ? "i" : "") + (regex.multiLine ? "m" : "") 21 | ); 22 | if (typeof startpos == "undefined") { 23 | startpos = this.length; 24 | } else if (startpos < 0) { 25 | startpos = 0; 26 | } 27 | var stringToWorkWith = string.substring(0, startpos + 1); 28 | var lastIndexOf = -1; 29 | var nextStop = 0; 30 | while ((result = regex.exec(stringToWorkWith)) != null) { 31 | lastIndexOf = result.index; 32 | regex.lastIndex = ++nextStop; 33 | } 34 | return lastIndexOf; 35 | } 36 | 37 | determineStringIsLike(val1, val2) { 38 | //TODO: LIKE fails on reserved regex characters (., +, etc) 39 | let regex = this.replaceAll(val2, "%", ".*"); 40 | regex = this.replaceAll(regex, "_", ".{1}"); 41 | // regex= this.replaceAll(regex,'\+','\+'); 42 | let re = new RegExp("^" + regex + "$", "g"); 43 | return re.test(val1); 44 | } 45 | 46 | getParsedValue(stringVal, quotesMandatory) { 47 | if (!isNaN(stringVal)) { 48 | return parseFloat(stringVal); 49 | } else if (stringVal === "true" || stringVal === "false") { 50 | return stringVal === "true"; 51 | } else if (stringVal === "null") { 52 | return null; 53 | } else if (Object.keys(SQL_FUNCTIONS).includes(stringVal.toLowerCase())) { 54 | return SQL_FUNCTIONS[stringVal.toLowerCase()](); 55 | } else if (quotesMandatory) { 56 | stringVal = stringVal.trim(); 57 | if (stringVal.match(/^["|'].+["|']$/)) { 58 | return stringVal.replace(/["']/g, ""); 59 | } else if (this.isMath(stringVal)) { 60 | return this.executeFunction(stringVal); 61 | } else { 62 | return { 63 | FIRESTATION_DATA_PROP: stringVal 64 | }; 65 | } 66 | } else { 67 | stringVal = stringVal.trim(); 68 | return stringVal.replace(/["']/g, ""); 69 | } 70 | } 71 | 72 | isMath(stringVal) { 73 | //TODO: 74 | return false || stringVal; 75 | } 76 | 77 | executeFunction(stringVal) { 78 | TODO: return null || stringVal; 79 | } 80 | 81 | // users/ => users, /users => users, users => users, / => / 82 | stripEncasingSlashes = string => { 83 | let str = string; 84 | if (str === "/") return str; 85 | let startIndex = 0; 86 | let endIndex = str.length; 87 | if (str.indexOf("/") === 0) startIndex++; 88 | if (str.charAt(str.length - 1) === "/") endIndex--; 89 | return str.substring(startIndex, endIndex); 90 | }; 91 | } 92 | 93 | export default new StringHelper(); 94 | 95 | const SQL_FUNCTIONS = { 96 | "rand()": () => Math.random() 97 | }; 98 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("@babel/polyfill"); 2 | import executeQuery from "./execute"; 3 | 4 | class FbSql { 5 | constructor() { 6 | this.app = null; 7 | this.isFirestore = false; 8 | this.shouldCommitResults = true; 9 | this.shouldExpandResults = false; 10 | } 11 | 12 | /** 13 | * @param {object} params - fbsql configuration 14 | * @param {object} [params.app] your firebase app 15 | * @param {boolean} [params.isFirestore] run queries against firestore? 16 | * @param {boolean} [params.shouldCommitResults] commit results on inserts, updates, deletes? 17 | * @param {boolean} [params.shouldExpandResults] return query info other than payload? 18 | */ 19 | configure = params => { 20 | params && 21 | Object.keys(params).forEach(key => { 22 | const val = params[key]; 23 | if (val || val === false) { 24 | this[key] = val; 25 | } 26 | }); 27 | }; 28 | 29 | killListeners = () => {}; 30 | 31 | getConfig = () => { 32 | return { 33 | app: this.app, 34 | isFirestore: this.isFirestore, 35 | shouldCommitResults: this.shouldCommitResults, 36 | shouldExpandResults: this.shouldExpandResults 37 | }; 38 | }; 39 | 40 | getApp = () => this.app; 41 | 42 | /** 43 | * @param {string} query - SQL query to execute against firebase 44 | * @param {function} [callback] - optional results callback, applies a listener 45 | * @param {boolean} [shouldApplyListener] - passing false after callback prevents listening 46 | * @returns {Promise} Promise object with results 47 | */ 48 | execute = (query, callback, shouldApplyListener) => { 49 | if (!query) 50 | throw new Error( 51 | `Must provide a string query argument, ie: execute("SELECT * FROM users")` 52 | ); 53 | return executeQuery( 54 | query, 55 | callback, 56 | callback && shouldApplyListener !== false 57 | ); 58 | }; 59 | } 60 | 61 | let fbsql = new FbSql(); 62 | 63 | const { configure: configureFbsql, getApp, getConfig } = fbsql; 64 | 65 | export { configureFbsql, getConfig, getApp }; 66 | export default fbsql.execute; 67 | -------------------------------------------------------------------------------- /src/models/fbSqlQuery.js: -------------------------------------------------------------------------------- 1 | export default class FbSqlQuery { 2 | constructor() { 3 | this.rawQuery = null; 4 | this.collection = null; 5 | this.path = null; 6 | this.selectedFields = null; 7 | this.wheres = null; 8 | this.orderBys = null; 9 | this.isFirestore = false; 10 | this.shouldApplyListener = false; 11 | this.shouldCommitResults = false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/parser/__tests__/query_parser.test.js: -------------------------------------------------------------------------------- 1 | import queryParser from "../query_parser"; 2 | import { 3 | NO_EQUALITY_STATEMENTS, 4 | SELECT_STATEMENT, 5 | INSERT_STATEMENT, 6 | UPDATE_STATEMENT, 7 | DELETE_STATEMENT 8 | } from "../../constants"; 9 | import { configureFbsql } from "../.."; 10 | 11 | test("clean comments", () => { 12 | const query = ` 13 | // this is a comment 14 | select * from users; -- another comment 15 | -- delete from users; 16 | // who woulda guessed it? a comment`; 17 | const cleaned = queryParser.formatAndCleanQuery(query); 18 | expect(cleaned).toBe("select * from users;"); 19 | }); 20 | 21 | test("determine statement type", () => { 22 | const { determineStatementType: dst } = queryParser; 23 | // SELECT 24 | expect(dst(`select * from users;`)).toBe(SELECT_STATEMENT); 25 | expect(dst(`users.insert.xyz`)).toBe(SELECT_STATEMENT); 26 | expect(dst(`/`)).toBe(SELECT_STATEMENT); 27 | expect(dst(`.`)).toBe(SELECT_STATEMENT); 28 | // INSERT 29 | expect(dst(`insert into x (a) values 1;`)).toBe(INSERT_STATEMENT); 30 | expect(dst(`insert into x (a) values (select a from b);`)).toBe( 31 | INSERT_STATEMENT 32 | ); 33 | // UPDATE 34 | expect(dst(`update a set b=1;`)).toBe(UPDATE_STATEMENT); 35 | expect(dst(`update a set b=(select b from a.id.b);`)).toBe(UPDATE_STATEMENT); 36 | // DELETE 37 | expect(dst(`delete from b`)).toBe(DELETE_STATEMENT); 38 | expect(dst(`delete from b where x =(select a from c)`)).toBe( 39 | DELETE_STATEMENT 40 | ); 41 | }); 42 | 43 | test("get wheres", done => { 44 | const query = `select * from users 45 | where age<=15 46 | and height not like "%tall" 47 | and isCool=false;`; 48 | queryParser.getWheres(query, wheres => { 49 | //otimizeWheres will reorder the results to make an equality statement first 50 | expect(wheres).toEqual([ 51 | { 52 | comparator: "==", 53 | field: "isCool", 54 | value: false 55 | }, 56 | { 57 | comparator: "!like", 58 | field: "height", 59 | value: "%tall" 60 | }, 61 | { 62 | comparator: "<=", 63 | field: "age", 64 | value: 15 65 | } 66 | ]); 67 | done(); 68 | }); 69 | }); 70 | 71 | test("get wheres - no equality check", done => { 72 | // optimize wheres detects when theres no equality check, 73 | // firebase will fetch entire collection 74 | const query = `select * from collection 75 | where age<=15 76 | and height not like "%tall";`; 77 | queryParser.getWheres(query, wheres => { 78 | expect(wheres).toEqual([ 79 | { 80 | error: NO_EQUALITY_STATEMENTS 81 | }, 82 | { 83 | comparator: "<=", 84 | field: "age", 85 | value: 15 86 | }, 87 | { 88 | comparator: "!like", 89 | field: "height", 90 | value: "%tall" 91 | } 92 | ]); 93 | done(); 94 | }); 95 | }); 96 | 97 | test("get sets", async () => { 98 | const query = `update users set height=10, name= "timmy" 99 | where age<5`; 100 | const sets = await queryParser.getSets(query); 101 | expect(sets).toEqual({ height: 10, name: "timmy" }); 102 | }); 103 | 104 | test("get order bys", () => { 105 | const query = `select * from lol 106 | where x < 15 and y=false 107 | order by age desc, name, y DESC`; 108 | const orderBys = queryParser.getOrderBys(query); 109 | expect(orderBys).toEqual([ 110 | { propToSort: "age", ascending: false }, 111 | { propToSort: "name", ascending: true }, 112 | { propToSort: "y", ascending: false } 113 | ]); 114 | }); 115 | 116 | test("get collection", () => { 117 | const { getCollection: getCol } = queryParser; 118 | // SELECT 119 | expect(getCol(`select * from lol;`, SELECT_STATEMENT)).toBe("lol"); 120 | expect(getCol(`select * from lol/;`, SELECT_STATEMENT)).toBe("lol"); 121 | expect(getCol(`select * from /lol/;`, SELECT_STATEMENT)).toBe("lol"); 122 | expect(getCol(`select * from /lol;`, SELECT_STATEMENT)).toBe("lol"); 123 | expect( 124 | getCol(`select * from c where age = (select a from q);`, SELECT_STATEMENT) 125 | ).toBe("c"); 126 | // INSERT 127 | expect(getCol(`insert into / (a) values (1);`, INSERT_STATEMENT)).toBe("/"); 128 | // UPDATE 129 | expect(getCol(`update b.d set age = 2;`, UPDATE_STATEMENT)).toBe("b/d"); 130 | // DELETE 131 | expect(getCol(`delete from wow;`, SELECT_STATEMENT)).toBe("wow"); 132 | expect(getCol(`delete from wow/;`, SELECT_STATEMENT)).toBe("wow"); 133 | expect(getCol(`select * from /;`, SELECT_STATEMENT)).toBe("/"); 134 | expect(getCol(`/`, SELECT_STATEMENT)).toBe("/"); 135 | expect(getCol(`/;`, SELECT_STATEMENT)).toBe("/"); 136 | expect(getCol(`.`, SELECT_STATEMENT)).toBe("/"); 137 | expect(getCol(`.;`, SELECT_STATEMENT)).toBe("/"); 138 | }); 139 | 140 | test("get selected fields", () => { 141 | const { getSelectedFields } = queryParser; 142 | expect(getSelectedFields(`/`)).toBeNull(); 143 | expect(getSelectedFields(`select * from lol;`)).toBeNull(); 144 | expect(getSelectedFields(`select age from users`)).toEqual({ age: true }); 145 | expect(getSelectedFields(`select a, q from users`)).toEqual({ 146 | a: true, 147 | q: true 148 | }); 149 | }); 150 | 151 | test("get insert count", () => { 152 | const { getInsertCount } = queryParser; 153 | expect(getInsertCount("insert 65 into col (a) values (1);")).toEqual(65); 154 | }); 155 | 156 | test("get not equal index", () => { 157 | const { getNotEqualIndex } = queryParser; 158 | let where = `where a !=5`; 159 | expect(getNotEqualIndex(where)).toEqual(where.indexOf("!=")); 160 | where = ` where a<>5 `; 161 | expect(getNotEqualIndex(where)).toEqual(where.indexOf("<>")); 162 | }); 163 | 164 | test("optimize wheres", () => { 165 | // function looks at wheres and determines which ones firebase 166 | // can filter by and moves that to index 0 167 | const { optimizeWheres } = queryParser; 168 | const a = { 169 | comparator: "<=", 170 | field: "a", 171 | value: 15 172 | }; 173 | const b = { 174 | comparator: "==", 175 | field: "b", 176 | value: 1 177 | }; 178 | // realtime db needs == where placed first 179 | expect(optimizeWheres([a, b], false)).toEqual([b, a]); 180 | // firestore is able to filter by <=, shouldn't be rearranged 181 | expect(optimizeWheres([a, b], true)).toEqual([a, b]); 182 | // no filterable wheres results in an error obj at index 0 183 | const err = { error: NO_EQUALITY_STATEMENTS }; 184 | expect(optimizeWheres([a], false)).toEqual([err, a]); 185 | }); 186 | 187 | test("check for cross db query", () => { 188 | // in case users want to query firestore when in realtime mode 189 | // and vice versa, ie: insert into firestore.users (select * from users); 190 | const { checkForCrossDbQuery: isCross } = queryParser; 191 | let { collection, isFirestore } = isCross(`users`); 192 | expect(collection).toBe(`users`); 193 | expect(isFirestore).toBeFalsy(); 194 | 195 | let { collection: col2, isFirestore: fs2 } = isCross(`firestore/users`); 196 | expect(col2).toBe(`users`); 197 | expect(fs2).toBeTruthy(); 198 | 199 | let { collection: col3, isFirestore: fs3 } = isCross(`db/users`); 200 | expect(col3).toBe(`users`); 201 | expect(fs3).toBeFalsy(); 202 | 203 | configureFbsql({ isFirestore: true }); 204 | let { collection: col4, isFirestore: fs4 } = isCross(`users`); 205 | expect(col4).toBe(`users`); 206 | expect(fs4).toBeTruthy(); 207 | 208 | let { collection: col5, isFirestore: fs5 } = isCross(`db/users`); 209 | expect(col5).toBe(`users`); 210 | expect(fs5).toBeFalsy(); 211 | }); 212 | -------------------------------------------------------------------------------- /src/parser/__tests__/query_parser_int.test.js: -------------------------------------------------------------------------------- 1 | // int tests where it executes against firebase: 2 | // getWheres() : select * from users where age = (select age from user.xyz); 3 | // getObjectsFromInsert() : insert into x selct * from y 4 | 5 | test("fake", () => { 6 | expect(true).toBeTruthy(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/parser/query_parser.js: -------------------------------------------------------------------------------- 1 | import stringHelper from "../helpers/string_helper"; 2 | import { 3 | SELECT_STATEMENT, 4 | UPDATE_STATEMENT, 5 | INSERT_STATEMENT, 6 | DELETE_STATEMENT, 7 | NO_EQUALITY_STATEMENTS 8 | } from "../constants"; 9 | import { getConfig } from "../index"; 10 | import executeSelect from "../query_runners/select"; 11 | import executeQuery from "../execute"; 12 | 13 | class QueryParser { 14 | formatAndCleanQuery(query) { 15 | query = stringHelper.replaceAll(query, /(\/\/|--).+/, ""); 16 | query = query.replace(/\r?\n|\r/g, " "); 17 | query = query.trim(); 18 | query = this.removeWrappedParenthesis(query); 19 | return query; 20 | } 21 | 22 | removeWrappedParenthesis(query) { 23 | return /^\(.+\)$/.test(query) 24 | ? query.substring(1, query.length - 1) 25 | : query; 26 | } 27 | 28 | determineStatementType(query) { 29 | let q = query.trim(); 30 | let firstTerm = q 31 | .split(" ")[0] 32 | .trim() 33 | .toLowerCase(); 34 | switch (firstTerm) { 35 | case "select": 36 | return SELECT_STATEMENT; 37 | case "update": 38 | return UPDATE_STATEMENT; 39 | case "insert": 40 | return INSERT_STATEMENT; 41 | case "delete": 42 | return DELETE_STATEMENT; 43 | default: 44 | return SELECT_STATEMENT; 45 | } 46 | } 47 | 48 | getWheres(query, callback) { 49 | const whereIndexStart = query.toUpperCase().indexOf(" WHERE ") + 1; 50 | if (whereIndexStart < 1) { 51 | return callback(null); 52 | } 53 | const orderByIndex = query.toUpperCase().indexOf("ORDER BY"); 54 | const whereIndexEnd = orderByIndex >= 0 ? orderByIndex : query.length; 55 | let wheresArr = query 56 | .substring(whereIndexStart + 5, whereIndexEnd) 57 | .split(/\sand\s/i); 58 | wheresArr[wheresArr.length - 1] = wheresArr[wheresArr.length - 1].replace( 59 | ";", 60 | "" 61 | ); 62 | let wheres = []; 63 | wheresArr.forEach(where => { 64 | where = stringHelper.replaceAllIgnoreCase(where, "not like", "!like"); 65 | let eqCompAndIndex = this.determineComparatorAndIndex(where); 66 | let whereObj = { 67 | field: stringHelper.replaceAll( 68 | where.substring(0, eqCompAndIndex.index).trim(), 69 | "\\.", 70 | "/" 71 | ), 72 | comparator: eqCompAndIndex.comparator 73 | }; 74 | const comparatorLength = 75 | eqCompAndIndex.comparator == "==" 76 | ? 1 77 | : eqCompAndIndex.comparator.length; 78 | const unparsedVal = where 79 | .substring(eqCompAndIndex.index + comparatorLength) 80 | .trim(); 81 | let val = stringHelper.getParsedValue(unparsedVal); 82 | const isFirestore = getConfig().isFirestore; 83 | if ( 84 | typeof val === "string" && 85 | val.charAt(0) === "(" && 86 | val.charAt(val.length - 1) === ")" 87 | ) { 88 | executeSelect(val.substring(1, val.length - 1), results => { 89 | whereObj.value = results.payload; 90 | wheres.push(whereObj); 91 | if (wheresArr.length === wheres.length) { 92 | return callback(this.optimizeWheres(wheres, isFirestore)); 93 | } 94 | }); 95 | } else { 96 | whereObj.value = val; 97 | wheres.push(whereObj); 98 | if (wheresArr.length === wheres.length) { 99 | return callback(this.optimizeWheres(wheres, isFirestore)); 100 | } 101 | } 102 | }); 103 | } 104 | 105 | async getSets(query) { 106 | const setIndexStart = query.indexOf(" set ") + 1; 107 | if (setIndexStart < 1) { 108 | return null; 109 | } 110 | const whereIndexStart = query.indexOf(" where ") + 1; 111 | let setsArr; 112 | if (whereIndexStart > 0) { 113 | setsArr = query.substring(setIndexStart + 3, whereIndexStart).split(", "); 114 | } else { 115 | setsArr = query.substring(setIndexStart + 3).split(", "); 116 | setsArr[setsArr.length - 1] = setsArr[setsArr.length - 1].replace( 117 | ";", 118 | "" 119 | ); 120 | } 121 | let sets = {}; 122 | setsArr.forEach(async item => { 123 | let [key, val] = item.split("="); 124 | if (key && val) { 125 | //set based on select data: update users set field = select field from users.id; 126 | if (/^\s*\(?(select).+from.+\)?/i.test(val)) { 127 | val = await executeQuery(val); 128 | } 129 | key = key.replace(".", "/").trim(); 130 | sets[key] = stringHelper.getParsedValue(val.trim(), true); 131 | } 132 | }); 133 | return sets; 134 | } 135 | 136 | getOrderBys(query) { 137 | let caps = query.toUpperCase(); 138 | const ORDER_BY = "ORDER BY"; 139 | let index = caps.indexOf(ORDER_BY); 140 | if (index < 0) { 141 | return null; 142 | } 143 | let orderByStr = query.substring(index + ORDER_BY.length); 144 | let split = orderByStr.split(","); 145 | let orderBys = split.map(orderBy => { 146 | let propToSort = orderBy.replace(";", "").trim(); 147 | propToSort = 148 | propToSort.indexOf(" ") >= 0 149 | ? propToSort.substring(0, propToSort.indexOf(" ")) 150 | : propToSort; 151 | let orderByObj = { 152 | ascending: true, 153 | propToSort: propToSort.trim() 154 | }; 155 | if (orderBy.toUpperCase().includes("DESC")) { 156 | orderByObj.ascending = false; 157 | } 158 | return orderByObj; 159 | }); 160 | return orderBys; 161 | } 162 | 163 | getCollection(q, statementType) { 164 | let query = q.replace(/\(.*\)/, "").trim(); //removes nested selects 165 | let terms = query.split(" "); 166 | const { stripEncasingSlashes: strip } = stringHelper; 167 | if (statementType === UPDATE_STATEMENT) { 168 | return strip(stringHelper.replaceAll(terms[1], /\./, "/")); 169 | } else if (statementType === SELECT_STATEMENT) { 170 | if (terms.length === 2 && terms[0] === "from") { 171 | return strip(stringHelper.replaceAll(terms[1], ".", "/")); 172 | } else if (terms.length === 1) { 173 | let collection = terms[0].replace(";", ""); 174 | return strip(stringHelper.replaceAll(collection, /\./, "/")); 175 | } 176 | let collectionIndexStart = query.indexOf("from ") + 4; 177 | if (collectionIndexStart < 0) { 178 | throw "Error determining collection."; 179 | } 180 | if (collectionIndexStart < 5) { 181 | return strip(stringHelper.replaceAll(terms[0], /\./, "/")); 182 | } 183 | let trimmedCol = query.substring(collectionIndexStart).trim(); 184 | let collectionIndexEnd = trimmedCol.match(/\ |;|$/).index; 185 | let collection = trimmedCol.substring(0, collectionIndexEnd); 186 | return strip(stringHelper.replaceAll(collection, /\./, "/")); 187 | } else if (statementType === INSERT_STATEMENT) { 188 | let collectionToInsert = 189 | terms[1].toUpperCase() === "INTO" ? terms[2] : terms[3]; 190 | return strip(stringHelper.replaceAll(collectionToInsert, /\./, "/")); 191 | } else if (statementType === DELETE_STATEMENT) { 192 | let index = terms.length > 2 ? 2 : 1; 193 | let term = stringHelper.replaceAll(terms[index], /;/, ""); 194 | return strip(stringHelper.replaceAll(term, /\./, "/")); 195 | } 196 | throw "Error determining collection."; 197 | } 198 | 199 | getSelectedFields(q) { 200 | let query = q.trim(); 201 | if (!query.startsWith("select ") || query.startsWith("select *")) { 202 | return null; 203 | } 204 | let regExp = /(.*select\s+)(.*)(\s+from.*)/; 205 | let froms = query.replace(regExp, "$2"); 206 | if (froms.length === query.length) { 207 | return null; 208 | } 209 | let fields = froms.split(","); 210 | if (fields.length === 0) { 211 | return null; 212 | } 213 | let selectedFields = {}; 214 | fields.map(field => { 215 | selectedFields[field.trim()] = true; 216 | }); 217 | return selectedFields; 218 | } 219 | 220 | getObjectsFromInsert(query, callback) { 221 | // const shouldApplyListener = getConfig().shouldCommitResults; 222 | 223 | //insert based on select data 224 | if (/^(insert into )[^\s]+( select).+/i.test(query)) { 225 | const selectStatement = query 226 | .substring(query.toUpperCase().indexOf("SELECT ")) 227 | .trim(); 228 | executeSelect(selectStatement, selectData => { 229 | return callback(selectData.payload || selectData); 230 | }); 231 | } else { 232 | //traditional insert 233 | let keysStr = query.substring(query.indexOf("(") + 1, query.indexOf(")")); 234 | let keys = keysStr.split(","); 235 | let valuesStr = query.match(/(values).+\)/)[0]; 236 | let valuesStrArr = valuesStr.split(/[\(](?!\))/); //splits on "(", unless its a function "func()" 237 | valuesStrArr.shift(); //removes "values (" 238 | let valuesArr = valuesStrArr.map(valueStr => { 239 | return valueStr.substring(0, valueStr.lastIndexOf(")")).split(","); 240 | }); 241 | if (!keys || !valuesArr) { 242 | throw "Badly formatted insert statement"; 243 | } 244 | let insertObjects = {}; 245 | valuesArr.forEach((values, valuesIndex) => { 246 | let insertObject = {}; 247 | keys.forEach((key, keyIndex) => { 248 | insertObject[ 249 | stringHelper.getParsedValue(key.trim()) 250 | ] = stringHelper.getParsedValue(values[keyIndex].trim()); 251 | }); 252 | insertObjects["pushId_" + valuesIndex] = insertObject; 253 | }); 254 | 255 | return callback(insertObjects); 256 | } 257 | } 258 | 259 | determineComparatorAndIndex(where) { 260 | let notEqIndex = this.getNotEqualIndex(where); 261 | if (notEqIndex >= 0) { 262 | return { comparator: "!=", index: notEqIndex }; 263 | } 264 | 265 | let greaterThanEqIndex = where.indexOf(">="); 266 | if (greaterThanEqIndex >= 0) { 267 | return { comparator: ">=", index: greaterThanEqIndex }; 268 | } 269 | 270 | let greaterThanIndex = where.indexOf(">"); 271 | if (greaterThanIndex >= 0) { 272 | return { comparator: ">", index: greaterThanIndex }; 273 | } 274 | 275 | let lessThanEqIndex = where.indexOf("<="); 276 | if (lessThanEqIndex >= 0) { 277 | return { comparator: "<=", index: lessThanEqIndex }; 278 | } 279 | let lessThanIndex = where.indexOf("<"); 280 | if (lessThanIndex >= 0) { 281 | return { comparator: "<", index: lessThanIndex }; 282 | } 283 | 284 | let notLikeIndex = where.toLowerCase().indexOf("!like"); 285 | if (notLikeIndex >= 0) { 286 | return { comparator: "!like", index: notLikeIndex }; 287 | } 288 | 289 | let likeIndex = where.toLowerCase().indexOf("like"); 290 | if (likeIndex >= 0) { 291 | return { comparator: "like", index: likeIndex }; 292 | } 293 | 294 | let eqIndex = where.indexOf("="); 295 | if (eqIndex >= 0) { 296 | return { comparator: "==", index: eqIndex }; 297 | } 298 | 299 | throw "Unrecognized comparator in where clause: '" + where + "'."; 300 | } 301 | 302 | getInsertCount(query) { 303 | const splitQ = query.trim().split(" "); 304 | if (splitQ[0].toUpperCase() === "INSERT" && parseInt(splitQ[1]) > 1) { 305 | return parseInt(splitQ[1]); 306 | } 307 | return 1; 308 | } 309 | 310 | getNotEqualIndex(condition) { 311 | return stringHelper.regexIndexOf(condition, /!=|<>/); 312 | } 313 | 314 | optimizeWheres(wheres, isFirestore) { 315 | const queryableComparators = isFirestore 316 | ? ["==", "<", "<=", ">", ">="] 317 | : ["=="]; 318 | 319 | //rearranges wheres so first statement is an equal, or error if no equals 320 | //firebase has no != method, so we'll grab whole collection, and filter on client 321 | const firstNotEqStatement = wheres[0]; 322 | for (let i = 0; i < wheres.length; i++) { 323 | if ( 324 | wheres[i].value != null && 325 | queryableComparators.includes(wheres[i].comparator) 326 | ) { 327 | wheres[0] = wheres[i]; 328 | wheres[i] = firstNotEqStatement; 329 | return wheres; 330 | } 331 | } 332 | 333 | wheres.unshift({ error: NO_EQUALITY_STATEMENTS }); 334 | return wheres; 335 | } 336 | 337 | checkForCrossDbQuery(collection) { 338 | let isFirestore = getConfig().isFirestore; 339 | if (/(db|firestore)/i.test(collection)) { 340 | if ( 341 | // only flip the db if it's not already enabled 342 | (isFirestore && /(db)/i.test(collection)) || 343 | (!isFirestore && /(firestore)/i.test(collection)) 344 | ) { 345 | isFirestore = !isFirestore; 346 | } 347 | collection = collection.substring(collection.indexOf("/") + 1); 348 | if (collection === "db" || collection === "firestore") { 349 | collection = "/"; 350 | } 351 | } 352 | return { collection, isFirestore }; 353 | } 354 | } 355 | 356 | const querParser = new QueryParser(); 357 | export default querParser; 358 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/firestore/delete_firestore_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeQuery from "../../../execute"; 3 | 4 | let testData; 5 | 6 | beforeEach(async () => { 7 | await clearDb(true); 8 | testData = { 9 | collection1: { 10 | item1: { 11 | a: 1, 12 | b: false 13 | }, 14 | item2: { 15 | a: 2, 16 | b: true 17 | } 18 | }, 19 | collection2: { 20 | item3: { 21 | c: 3 22 | } 23 | } 24 | }; 25 | await injectData("/collection1", testData.collection1, true); 26 | await injectData("/collection2", testData.collection2, true); 27 | }); 28 | 29 | afterAll(async () => { 30 | await clearDb(true); 31 | }); 32 | 33 | test("delete entire collection", async () => { 34 | await executeQuery("delete from collection1/"); 35 | const { collection1, collection2 } = await executeQuery("select * from /;"); 36 | expect(collection1).toEqual(undefined); 37 | expect(collection2).toEqual(testData.collection2); 38 | }); 39 | 40 | test("delete one child object", async () => { 41 | await executeQuery("delete from collection1.item1"); 42 | const collection1 = await executeQuery("select * from collection1;"); 43 | delete testData.collection1.item1; 44 | expect(collection1).toEqual(testData.collection1); 45 | }); 46 | 47 | test("delete one prop", async () => { 48 | await executeQuery("delete from collection1.item1.a"); 49 | const collection1 = await executeQuery("select * from collection1;"); 50 | delete testData.collection1.item1.a; 51 | expect(collection1).toEqual(testData.collection1); 52 | }); 53 | 54 | test("delete where condition", async () => { 55 | await executeQuery("delete from collection1 where b = true;"); 56 | const collection1 = await executeQuery("select * from collection1;"); 57 | delete testData.collection1.item2; 58 | expect(collection1).toEqual(testData.collection1); 59 | }); 60 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/firestore/insert_firestore_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb } from "../../test_resources/setup_db"; 2 | import executeInsert from "../../insert"; 3 | import executeSelect from "../../select"; 4 | import { configureFbsql } from "../../.."; 5 | 6 | beforeEach(async () => { 7 | configureFbsql({ isFirestore: true }); 8 | await clearDb(true); 9 | }); 10 | 11 | afterAll(async () => { 12 | await clearDb(true); 13 | }); 14 | 15 | test("standard insert", async () => { 16 | await executeInsert("insert into k (a) values ('b');"); 17 | const res = await executeSelect("select * from k;"); 18 | const val = res && res[Object.keys(res)[0]]; 19 | expect(val).toEqual({ a: "b" }); 20 | }); 21 | 22 | test("callback based insert", done => { 23 | executeInsert("insert into g (i) values ('d');", res => { 24 | executeSelect( 25 | "select * from g;", 26 | res => { 27 | const val = res && res[Object.keys(res)[0]]; 28 | expect(val).toEqual({ i: "d" }); 29 | done(); 30 | }, 31 | false 32 | ); 33 | }); 34 | }); 35 | 36 | test("insert based on select data", async () => { 37 | await executeInsert("insert into col (g) values ('h');"); 38 | await executeInsert("insert into col2 select * from col;"); 39 | const res = await executeSelect("select * from col2;"); 40 | const val = res && res[Object.keys(res)[0]]; 41 | expect(val).toEqual({ g: "h" }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/firestore/select_firestore_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeSelect from "../../select"; 3 | import { configureFbsql } from "../../../index"; 4 | 5 | const users = { 6 | abc: { 7 | email: "ab@c.com", 8 | age: 20, 9 | isOnline: false 10 | }, 11 | def: { 12 | email: "de@ef.gov", 13 | age: 25, 14 | isOnline: true, 15 | bio: "what a guy" 16 | } 17 | }; 18 | 19 | beforeEach(async () => { 20 | configureFbsql({ shouldExpandResults: false, isFirestore: true }); 21 | await clearDb(true); 22 | await injectData("users", users, true); 23 | }); 24 | 25 | afterAll(async () => { 26 | await clearDb(true); 27 | }); 28 | 29 | // important: must make sure callback queries are not listeners, 30 | // otherwise they'll refire when other tests alter the database. 31 | // can pass false after callback arg to prevent a listener 32 | 33 | test("callback working", done => { 34 | configureFbsql({ shouldExpandResults: true }); 35 | executeSelect( 36 | "select * from users", 37 | ({ payload, firebaseListener }) => { 38 | expect(payload).toEqual(users); 39 | done(); 40 | }, 41 | false 42 | ); 43 | }); 44 | 45 | test("async working", async () => { 46 | const data = await executeSelect("select * from users"); 47 | expect(users).toEqual(data); 48 | }); 49 | 50 | test("select specific property", async () => { 51 | const data = await executeSelect("select * from users.abc.age"); 52 | expect(data).toEqual(users.abc.age); 53 | }); 54 | 55 | test("select certain fields", async () => { 56 | const data = await executeSelect("select email, age from users"); 57 | Object.keys(data).forEach(id => { 58 | const user = data[id]; 59 | expect(user.age).toBeTruthy(); 60 | expect(user.email).toBeTruthy(); 61 | expect(user.isOnline).toBeUndefined(); 62 | expect(user.bio).toBeUndefined(); 63 | }); 64 | }); 65 | 66 | test("expanded results working", done => { 67 | configureFbsql({ shouldExpandResults: true }); 68 | executeSelect("select * from users.def", results => { 69 | const { path, payload, firebaseListener } = results; 70 | expect(payload).toEqual(users.def); 71 | expect("users/def").toEqual(path); 72 | expect(typeof firebaseListener).toEqual("object"); 73 | expect(typeof firebaseListener.unsubscribe).toEqual("function"); 74 | firebaseListener.unsubscribe(); 75 | configureFbsql({ shouldExpandResults: false }); 76 | done(); 77 | }); 78 | }); 79 | 80 | test("where queries working", async () => { 81 | const results = await executeSelect("select * from users where age=25"); 82 | expect({ def: users.def }).toEqual(results); 83 | }); 84 | 85 | test("string regex", async () => { 86 | const results = await executeSelect( 87 | `select * from users where email like %@c.com` 88 | ); 89 | expect({ abc: users.abc }).toEqual(results); 90 | }); 91 | 92 | test("less than operator", async () => { 93 | const results = await executeSelect(`select * from users where age <25;`); 94 | expect({ abc: users.abc }).toEqual(results); 95 | }); 96 | 97 | test("less than equal to operator", async () => { 98 | const results = await executeSelect(`select * from users where age <=25;`); 99 | expect(users).toEqual(results); 100 | }); 101 | 102 | test("greater than operator", async () => { 103 | const results = await executeSelect(`select * from users where age >20;`); 104 | expect({ def: users.def }).toEqual(results); 105 | }); 106 | 107 | test("greater than equal to operator", async () => { 108 | const results = await executeSelect(`select * from users where age>=20;`); 109 | expect(users).toEqual(results); 110 | }); 111 | 112 | test("query by null", async () => { 113 | const results = await executeSelect(`select * from users where bio=null;`); 114 | expect({ abc: users.abc }).toEqual(results); 115 | }); 116 | 117 | test("query by not null", async () => { 118 | const results = await executeSelect(`select * from users where bio!=null;`); 119 | expect({ def: users.def }).toEqual(results); 120 | }); 121 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/firestore/update_firestore_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeQuery from "../../../execute"; 3 | import { configureFbsql } from "../../.."; 4 | 5 | let localBlogs = {}; 6 | 7 | beforeEach(async () => { 8 | configureFbsql({ isFirestore: true }); 9 | await clearDb(true); 10 | localBlogs = { 11 | blog1: { 12 | title: "My first blog", 13 | description: "blog descrip" 14 | }, 15 | blog2: { 16 | title: "My second blog", 17 | description: "wowza" 18 | } 19 | }; 20 | await injectData("/blogs", localBlogs, true); 21 | }); 22 | 23 | afterAll(async () => { 24 | await clearDb(true); 25 | configureFbsql({ isFirestore: false }); 26 | }); 27 | 28 | test("firestore: update all", async () => { 29 | await executeQuery("update blogs set tall = true;"); 30 | const blogsRes = await executeQuery("select * from blogs;"); 31 | updateLocalBlogs({ tall: true }); 32 | expect(blogsRes).toEqual(localBlogs); 33 | }); 34 | 35 | test("firestore: select based update", async () => { 36 | await executeQuery( 37 | "update blogs set title = (select title from blogs.blog1);" 38 | ); 39 | const blogsRes = await executeQuery("select * from blogs;"); 40 | localBlogs.blog2.title = localBlogs.blog1.title; 41 | expect(blogsRes).toEqual(localBlogs); 42 | }); 43 | 44 | test("firestore: filtered update", async () => { 45 | await executeQuery("update blogs set length=15 where description = 'wowza';"); 46 | const blogsRes = await executeQuery("select * from blogs;"); 47 | localBlogs.blog2.length = 15; 48 | expect(blogsRes).toEqual(localBlogs); 49 | }); 50 | 51 | const updateLocalBlogs = (updates = {}) => { 52 | Object.keys(localBlogs).forEach(blogId => { 53 | Object.keys(updates).forEach(key => { 54 | localBlogs[blogId][key] = updates[key]; 55 | }); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/realtime/delete_realtime_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeQuery from "../../../execute"; 3 | 4 | let testData; 5 | 6 | beforeEach(async () => { 7 | await clearDb(); 8 | testData = { 9 | collection1: { 10 | item1: { 11 | a: 1, 12 | b: false 13 | }, 14 | item2: { 15 | a: 2, 16 | b: true 17 | } 18 | }, 19 | collection2: "fake" 20 | }; 21 | await injectData("/", testData); 22 | }); 23 | 24 | afterAll(async () => { 25 | await clearDb(); 26 | }); 27 | 28 | test("delete entire collection", async () => { 29 | await executeQuery("delete from collection1"); 30 | const { collection1, collection2 } = await executeQuery("select * from /;"); 31 | expect(collection1).toBeUndefined(); 32 | expect(collection2).toEqual(testData.collection2); 33 | }); 34 | 35 | test("delete one child object", async () => { 36 | await executeQuery("delete from collection1.item1"); 37 | const collection1 = await executeQuery("select * from collection1;"); 38 | delete testData.collection1.item1; 39 | expect(collection1).toEqual(testData.collection1); 40 | }); 41 | 42 | test("delete one prop", async () => { 43 | await executeQuery("delete from collection1.item1.a"); 44 | const collection1 = await executeQuery("select * from collection1;"); 45 | delete testData.collection1.item1.a; 46 | expect(collection1).toEqual(testData.collection1); 47 | }); 48 | 49 | test("delete where condition", async () => { 50 | await executeQuery("delete from collection1 where b = true;"); 51 | const collection1 = await executeQuery("select * from collection1;"); 52 | delete testData.collection1.item2; 53 | expect(collection1).toEqual(testData.collection1); 54 | }); 55 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/realtime/insert_realtime_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb } from "../../test_resources/setup_db"; 2 | import executeInsert from "../../insert"; 3 | import executeSelect from "../../select"; 4 | 5 | beforeEach(async () => { 6 | await clearDb(); 7 | }); 8 | 9 | afterAll(async () => { 10 | await clearDb(); 11 | }); 12 | 13 | test("standard insert", async () => { 14 | await executeInsert("insert into k (a) values ('b');"); 15 | const res = await executeSelect("select * from k;"); 16 | const val = res && res[Object.keys(res)[0]]; 17 | expect(val).toEqual({ a: "b" }); 18 | }); 19 | 20 | test("callback based insert", done => { 21 | executeInsert("insert into g (i) values ('d');", res => { 22 | executeSelect( 23 | "select * from g;", 24 | res => { 25 | const val = res && res[Object.keys(res)[0]]; 26 | expect(val).toEqual({ i: "d" }); 27 | done(); 28 | }, 29 | false 30 | ); 31 | }); 32 | }); 33 | 34 | test("insert based on select data", async () => { 35 | await executeInsert("insert into col (g) values ('h');"); 36 | await executeInsert("insert into col2 select * from col;"); 37 | const res = await executeSelect("select * from col2;"); 38 | const val = res && res[Object.keys(res)[0]]; 39 | expect(val).toEqual({ g: "h" }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/realtime/select_realtime_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeSelect from "../../select"; 3 | import { configureFbsql } from "../../../index"; 4 | 5 | const users = { 6 | abc: { 7 | email: "ab@c.com", 8 | age: 20, 9 | isOnline: false 10 | }, 11 | def: { 12 | email: "de@ef.gov", 13 | age: 25, 14 | isOnline: true, 15 | bio: "what a guy" 16 | } 17 | }; 18 | 19 | beforeEach(async () => { 20 | configureFbsql({ shouldExpandResults: false, isFirestore: false }); 21 | await clearDb(); 22 | await injectData("users", users); 23 | }); 24 | 25 | afterAll(async () => { 26 | await clearDb(); 27 | }); 28 | 29 | // important: must make sure callback queries are not listeners, 30 | // otherwise they'll refire when other tests alter the database. 31 | // can pass false after callback arg to prevent a listener 32 | 33 | test("callback working", done => { 34 | configureFbsql({ shouldExpandResults: true }); 35 | executeSelect( 36 | "select * from users", 37 | ({ payload, firebaseListener }) => { 38 | expect(payload).toEqual(users); 39 | done(); 40 | }, 41 | false 42 | ); 43 | }); 44 | 45 | test("async working", async () => { 46 | const data = await executeSelect("select * from users"); 47 | expect(users).toEqual(data); 48 | }); 49 | 50 | test("select specific property", async () => { 51 | const data = await executeSelect("select * from users.abc.age"); 52 | expect(users.abc.age).toEqual(data); 53 | }); 54 | 55 | test("select certain fields", async () => { 56 | const data = await executeSelect("select email, age from users"); 57 | Object.keys(data).forEach(id => { 58 | const user = data[id]; 59 | expect(user.age).toBeTruthy(); 60 | expect(user.email).toBeTruthy(); 61 | expect(user.isOnline).toBeUndefined(); 62 | expect(user.bio).toBeUndefined(); 63 | }); 64 | }); 65 | 66 | test("expanded results working", done => { 67 | configureFbsql({ shouldExpandResults: true }); 68 | executeSelect("select * from users.def", results => { 69 | const { path, payload, firebaseListener } = results; 70 | expect(payload).toEqual(users.def); 71 | expect("users/def").toEqual(path); 72 | expect(typeof firebaseListener).toEqual("object"); 73 | expect(typeof firebaseListener.unsubscribe).toEqual("function"); 74 | firebaseListener.unsubscribe(); 75 | configureFbsql({ shouldExpandResults: false }); 76 | done(); 77 | }); 78 | }); 79 | 80 | test("where queries working", async () => { 81 | const results = await executeSelect("select * from users where age=25"); 82 | expect({ def: users.def }).toEqual(results); 83 | }); 84 | 85 | test("string regex", async () => { 86 | const results = await executeSelect( 87 | `select * from users where email like %@c.com` 88 | ); 89 | expect({ abc: users.abc }).toEqual(results); 90 | }); 91 | 92 | test("less than operator", async () => { 93 | const results = await executeSelect(`select * from users where age <25;`); 94 | expect({ abc: users.abc }).toEqual(results); 95 | }); 96 | 97 | test("less than equal to operator", async () => { 98 | const results = await executeSelect(`select * from users where age <=25;`); 99 | expect(users).toEqual(results); 100 | }); 101 | 102 | test("greater than operator", async () => { 103 | const results = await executeSelect(`select * from users where age >20;`); 104 | expect({ def: users.def }).toEqual(results); 105 | }); 106 | 107 | test("greater than equal to operator", async () => { 108 | const results = await executeSelect(`select * from users where age>=20;`); 109 | expect(users).toEqual(results); 110 | }); 111 | 112 | test("query by null", async () => { 113 | const results = await executeSelect(`select * from users where bio=null;`); 114 | expect({ abc: users.abc }).toEqual(results); 115 | }); 116 | 117 | test("query by not null", async () => { 118 | const results = await executeSelect(`select * from users where bio!=null;`); 119 | expect({ def: users.def }).toEqual(results); 120 | }); 121 | -------------------------------------------------------------------------------- /src/query_runners/__tests__/realtime/update_realtime_int.test.js: -------------------------------------------------------------------------------- 1 | import { clearDb, injectData } from "../../test_resources/setup_db"; 2 | import executeQuery from "../../../execute"; 3 | import { configureFbsql } from "../../.."; 4 | 5 | let localBlogs = {}; 6 | 7 | beforeEach(async () => { 8 | configureFbsql({ isFirestore: false }); 9 | await clearDb(); 10 | localBlogs = { 11 | blog1: { 12 | title: "My first blog", 13 | description: "blog descrip" 14 | }, 15 | blog2: { 16 | title: "My second blog", 17 | description: "wowza" 18 | } 19 | }; 20 | await injectData("/blogs", localBlogs); 21 | }); 22 | 23 | afterAll(async () => { 24 | await clearDb(); 25 | }); 26 | 27 | test("update all", async () => { 28 | await executeQuery("update blogs set tall = true;"); 29 | const blogsRes = await executeQuery("select * from blogs;"); 30 | updateLocalBlogs({ tall: true }); 31 | expect(blogsRes).toEqual(localBlogs); 32 | }); 33 | 34 | test("filtered update", async () => { 35 | await executeQuery("update blogs set length=15 where description = 'wowza';"); 36 | const blogsRes = await executeQuery("select * from blogs;"); 37 | localBlogs.blog2.length = 15; 38 | expect(blogsRes).toEqual(localBlogs); 39 | }); 40 | 41 | test("select based update", async () => { 42 | await executeQuery( 43 | "update blogs set title = (select title from blogs.blog1);" 44 | ); 45 | const blogsRes = await executeQuery("select * from blogs;"); 46 | localBlogs.blog2.title = localBlogs.blog1.title; 47 | expect(blogsRes).toEqual(localBlogs); 48 | }); 49 | 50 | const updateLocalBlogs = (updates = {}) => { 51 | Object.keys(localBlogs).forEach(blogId => { 52 | Object.keys(updates).forEach(key => { 53 | localBlogs[blogId][key] = updates[key]; 54 | }); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /src/query_runners/delete.js: -------------------------------------------------------------------------------- 1 | import queryParser from "../parser/query_parser"; 2 | import { deleteObject } from "../db/update_db"; 3 | import { getDataForSelectAsync } from "../db/select_db"; 4 | import QueryDetails from "../models/fbSqlQuery"; 5 | import { DELETE_STATEMENT } from "../constants"; 6 | import { getConfig } from ".."; 7 | 8 | //TODO: refactor this away from firestation use case 9 | // no need to grab the data first > commit for most ppl 10 | export default function executeDelete(query, callback) { 11 | const col = queryParser.getCollection(query, DELETE_STATEMENT); 12 | const { collection, isFirestore } = queryParser.checkForCrossDbQuery(col); 13 | const commitResults = getConfig().shouldCommitResults; 14 | 15 | return new Promise((resolve, reject) => { 16 | queryParser.getWheres(query, async wheres => { 17 | let queryDetails = new QueryDetails(); 18 | queryDetails.collection = collection; 19 | queryDetails.isFirestore = isFirestore; 20 | queryDetails.wheres = wheres; 21 | const { payload, firebaseListener } = await getDataForSelectAsync( 22 | queryDetails 23 | ); 24 | 25 | if (payload && commitResults) { 26 | if (["boolean", "number", "string"].includes(typeof payload)) { 27 | // path is a non-obj data prop, ie: delete from users.userId.height; 28 | await deleteObject(collection, isFirestore); 29 | } else if (!wheres && collection.indexOf(`/`) > 0) { 30 | // unfiltered: delete from users.userId 31 | await deleteObject(collection, isFirestore); 32 | } else { 33 | // Use select payload to determine deletes: 34 | // entire col: delete from users; 35 | // OR filtered: delete from users where age > x; 36 | const deletePromises = []; 37 | Object.keys(payload).forEach(objKey => { 38 | const path = collection + "/" + objKey; 39 | deletePromises.push(deleteObject(path, isFirestore)); 40 | }); 41 | await Promise.all(deletePromises); 42 | } 43 | } 44 | let results = { 45 | statementType: DELETE_STATEMENT, 46 | payload: payload, 47 | firebaseListener: firebaseListener, 48 | path: collection 49 | }; 50 | callback ? callback(results) : resolve(results); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/query_runners/insert.js: -------------------------------------------------------------------------------- 1 | import queryParser from "../parser/query_parser"; 2 | import { pushObject } from "../db/update_db"; 3 | import { INSERT_STATEMENT } from "../constants"; 4 | import { getConfig } from ".."; 5 | 6 | export default function executeInsert(query, callback) { 7 | const col = queryParser.getCollection(query, INSERT_STATEMENT); 8 | const { collection, isFirestore } = queryParser.checkForCrossDbQuery(col); 9 | const insertCount = queryParser.getInsertCount(query); 10 | const path = collection + "/"; 11 | const commitResults = getConfig().shouldCommitResults; 12 | 13 | return new Promise((resolve, reject) => { 14 | queryParser.getObjectsFromInsert(query, async insertObjects => { 15 | if (commitResults) { 16 | let keys = insertObjects && Object.keys(insertObjects); 17 | const insertPromises = []; 18 | for (let i = 1; i < insertCount; i++) { 19 | //insert clones 20 | const prom = pushObject(path, insertObjects[keys[0]], isFirestore); 21 | insertPromises.push(prom); 22 | } 23 | for (let key in insertObjects) { 24 | const prom = pushObject(path, insertObjects[key], isFirestore); 25 | insertPromises.push(prom); 26 | } 27 | await Promise.all(insertPromises); 28 | } 29 | let results = { 30 | insertCount: insertCount, 31 | statementType: INSERT_STATEMENT, 32 | payload: insertObjects, 33 | path: path 34 | }; 35 | 36 | if (callback) callback(results); 37 | else { 38 | resolve(results); 39 | } 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/query_runners/select.js: -------------------------------------------------------------------------------- 1 | import queryParser from "../parser/query_parser"; 2 | import { getDataForSelect, getDataForSelectAsync } from "../db/select_db"; 3 | import QueryDetails from "../models/fbSqlQuery"; 4 | import { SELECT_STATEMENT } from "../constants"; 5 | import { getConfig } from "../index"; 6 | 7 | export default function executeSelect( 8 | query, 9 | callback, 10 | shouldApplyListener = true 11 | ) { 12 | const col = queryParser.getCollection(query, SELECT_STATEMENT); 13 | const { collection, isFirestore } = queryParser.checkForCrossDbQuery(col); 14 | 15 | let queryDetails = new QueryDetails(); 16 | queryDetails.collection = collection; 17 | queryDetails.isFirestore = isFirestore; 18 | queryDetails.orderBys = queryParser.getOrderBys(query); 19 | queryDetails.selectedFields = queryParser.getSelectedFields(query); 20 | queryDetails.shouldApplyListener = 21 | callback && shouldApplyListener ? true : false; 22 | 23 | return new Promise((resolve, reject) => { 24 | queryParser.getWheres(query, async wheres => { 25 | queryDetails.wheres = wheres; 26 | if (callback) { 27 | getDataForSelect(queryDetails, results => { 28 | callback(customizeResults(results)); 29 | }); 30 | } else { 31 | const results = await getDataForSelectAsync(queryDetails); 32 | resolve(customizeResults(results)); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | const customizeResults = results => 39 | getConfig().shouldExpandResults ? results : results.payload; 40 | -------------------------------------------------------------------------------- /src/query_runners/test_resources/db_config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | databaseURL: "", 3 | serviceAccount: {} // copy from service account json file 4 | }; 5 | -------------------------------------------------------------------------------- /src/query_runners/test_resources/setup_db.js: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | import config from "./db_config"; 3 | import executeQuery from "../../execute"; 4 | import { configureFbsql } from "../.."; 5 | 6 | const { databaseURL, serviceAccount } = config; 7 | 8 | admin.initializeApp({ 9 | credential: admin.credential.cert(serviceAccount), 10 | databaseURL 11 | }); 12 | configureFbsql({ app: admin }); 13 | 14 | const firestore = admin.firestore(); 15 | const settings = { timestampsInSnapshots: true }; 16 | firestore.settings(settings); 17 | 18 | export const clearDb = async isFirestore => { 19 | return isFirestore 20 | ? deleteFirestore() 21 | : admin 22 | .database() 23 | .ref("/") 24 | .set(null); 25 | }; 26 | 27 | export const injectData = (path, data, isFirestore) => { 28 | return isFirestore 29 | ? injectIntoFirestore(path, data) 30 | : admin 31 | .database() 32 | .ref(path) 33 | .set(data); 34 | }; 35 | 36 | const injectIntoFirestore = (path, data) => { 37 | const db = admin.firestore(); 38 | const batch = db.batch(); 39 | 40 | data && 41 | Object.keys(data).forEach(docTitle => { 42 | batch.set(db.collection(path).doc(docTitle), data[docTitle]); 43 | }); 44 | return batch.commit(); 45 | }; 46 | 47 | const deleteFirestore = async () => { 48 | configureFbsql({ isFirestore: true }); 49 | const rootData = await executeQuery("select * from /"); 50 | const db = admin.firestore(); 51 | const batch = db.batch(); 52 | rootData && 53 | Object.keys(rootData).forEach(colKey => { 54 | const collectionData = rootData[colKey]; 55 | collectionData && 56 | Object.keys(collectionData).forEach(docId => { 57 | batch.delete(db.collection(colKey).doc(docId)); 58 | }); 59 | }); 60 | return batch.commit(); 61 | }; 62 | -------------------------------------------------------------------------------- /src/query_runners/update.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | import queryParser from "../parser/query_parser"; 4 | import { updateFields } from "../db/update_db"; 5 | import { getDataForSelect } from "../db/select_db"; 6 | import { 7 | EQUATION_IDENTIFIERS, 8 | FIRESTATION_DATA_PROP, 9 | UPDATE_STATEMENT 10 | } from "../constants"; 11 | import QueryDetails from "../models/fbSqlQuery"; 12 | import { getConfig } from ".."; 13 | 14 | export default async function executeUpdate(query, callback) { 15 | const col = queryParser.getCollection(query, UPDATE_STATEMENT); 16 | const { collection, isFirestore } = queryParser.checkForCrossDbQuery(col); 17 | const commitResults = getConfig().shouldCommitResults; 18 | const sets = await queryParser.getSets(query); 19 | if (!sets) { 20 | return null; 21 | } 22 | // const that = this; do this for queryparser? 23 | return new Promise((resolve, reject) => { 24 | queryParser.getWheres(query, wheres => { 25 | let queryDetails = new QueryDetails(); 26 | queryDetails.collection = collection; 27 | queryDetails.isFirestore = isFirestore; 28 | // queryDetails.db = db; 29 | queryDetails.wheres = wheres; 30 | getDataForSelect(queryDetails, async dataToAlter => { 31 | let data = dataToAlter.payload; 32 | const payload = generatePayload(data, sets); 33 | const results = { 34 | statementType: UPDATE_STATEMENT, 35 | payload, 36 | firebaseListener: dataToAlter.firebaseListener, 37 | path: collection 38 | }; 39 | 40 | if (commitResults && data) { 41 | const updatePromises = []; 42 | Object.keys(data).forEach(objKey => { 43 | const updateObj = payload[objKey]; 44 | const path = collection + "/" + objKey; 45 | const updatePromise = updateFields( 46 | path, 47 | updateObj, 48 | Object.keys(sets), 49 | isFirestore 50 | ); 51 | updatePromises.push(updatePromise); 52 | }); 53 | await Promise.all(updatePromises); 54 | callback ? callback(results) : resolve(results); 55 | } else { 56 | callback ? callback(results) : resolve(results); 57 | } 58 | }); 59 | }); 60 | }); 61 | } 62 | 63 | const generatePayload = (data, sets) => { 64 | const payload = {}; 65 | data && 66 | Object.keys(data).forEach(objKey => { 67 | const updateObj = updateItemWithSets(data[objKey], sets); 68 | payload[objKey] = updateObj; 69 | }); 70 | return payload; 71 | }; 72 | 73 | export function updateItemWithSets(obj, sets) { 74 | const that = this; 75 | let updateObject = _.clone(obj); 76 | Object.keys(sets).forEach(objKey => { 77 | const thisSet = sets[objKey]; 78 | if ( 79 | thisSet && 80 | typeof thisSet === "object" && 81 | thisSet.hasOwnProperty(FIRESTATION_DATA_PROP) 82 | ) { 83 | //execute equation 84 | const newVal = thisSet.FIRESTATION_DATA_PROP; 85 | for (let i = 0; i < EQUATION_IDENTIFIERS.length; i++) { 86 | if (newVal.includes(EQUATION_IDENTIFIERS[i])) { 87 | updateObject[objKey] = that.executeUpdateEquation( 88 | updateObject, 89 | thisSet.FIRESTATION_DATA_PROP 90 | ); 91 | return updateObject; 92 | } 93 | } 94 | //not an equation, treat it as an individual prop 95 | let finalValue = updateObject[newVal]; 96 | if (newVal.includes(".")) { 97 | let props = newVal.split("."); 98 | finalValue = updateObject[props[0]]; 99 | for (let i = 1; updateObjecti < props.length; i++) { 100 | finalValue = finalValue[props[i]]; 101 | } 102 | } 103 | updateObject[objKey] = finalValue; 104 | } else { 105 | if (objKey.includes("/")) { 106 | // "users/userId/name" -> users: { userId: { name: ""}}, etc 107 | if (typeof updateObject !== "object") { 108 | updateObject = {}; 109 | } 110 | let currentObject = updateObject; 111 | let dataPath = objKey.split("/"); 112 | dataPath.forEach((val, i) => { 113 | if (i === dataPath.length - 1) { 114 | currentObject[val] = thisSet; 115 | } else { 116 | let currVal = currentObject[val]; 117 | 118 | currentObject[val] = 119 | currVal && typeof currVal === "object" ? currentObject[val] : {}; 120 | } 121 | currentObject = currentObject[val]; 122 | }); 123 | } else { 124 | updateObject[objKey] = thisSet; 125 | } 126 | } 127 | }); 128 | return updateObject; 129 | } 130 | --------------------------------------------------------------------------------