├── .flowconfig ├── .vscode └── settings.json ├── select.gif ├── app ├── config.js ├── assets │ ├── images │ │ ├── logo.ico │ │ └── Thumbs.db │ └── stylesheets │ │ └── base.scss ├── helpers │ ├── CacheHelper.js │ ├── JsonHelper.js │ ├── ObjectHelper.js │ ├── DateHelper.js │ ├── StringHelper.js │ └── QueryHelper.js ├── components │ ├── templates │ │ ├── Template.jsx │ │ └── FunctionalTemplate.jsx │ ├── modals │ │ ├── SaveQuery.jsx │ │ ├── Modal.jsx │ │ ├── AddDatabase.jsx │ │ └── DatabaseConfig.jsx │ ├── object_tree │ │ ├── ObjectTree.jsx │ │ └── ObjectNode.jsx │ ├── Navbar.jsx │ ├── QueryResults.jsx │ ├── QueryHistory.jsx │ ├── ButtonRow.jsx │ ├── Workbook.jsx │ ├── SideMenu.jsx │ ├── Workstation.jsx │ └── App.jsx ├── index.js ├── service │ ├── FirebaseService.js │ └── UpdateService.js ├── index.html └── stores │ └── Store.js ├── webpack.config.eslint.js ├── webpack.config.node.js ├── .editorconfig ├── .yarnclean ├── todo.md ├── .babelrc ├── README.md ├── .gitignore ├── webpack.config.js ├── server.js ├── webpack.config.electron.js ├── LICENSE ├── webpack.config.dev.js ├── webpack.config.prod.js ├── .eslintrc ├── package.js ├── package.json └── main.development.js /.flowconfig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": false 3 | } -------------------------------------------------------------------------------- /select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeRoddy/firestation/HEAD/select.gif -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | // export const CACHE_RESET = true; 2 | export const CACHE_RESET = false; -------------------------------------------------------------------------------- /app/assets/images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeRoddy/firestation/HEAD/app/assets/images/logo.ico -------------------------------------------------------------------------------- /app/assets/images/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeRoddy/firestation/HEAD/app/assets/images/Thumbs.db -------------------------------------------------------------------------------- /webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | module.exports = require('./webpack.config.dev') 4 | -------------------------------------------------------------------------------- /app/helpers/CacheHelper.js: -------------------------------------------------------------------------------- 1 | export default class CacheHelper { 2 | static updateLocalStore(key, obj) { 3 | localStorage.setItem(key, JSON.stringify(obj)); 4 | } 5 | 6 | static getFromLocalStore(key) { 7 | return JSON.parse(localStorage.getItem(key)); 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /app/helpers/JsonHelper.js: -------------------------------------------------------------------------------- 1 | export function convertJsonToDbConfig(details) { 2 | details = details.substring(details.indexOf('{')); 3 | details = details.substring(0, details.indexOf("}") + 1) 4 | details = details.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:(?!\/)/g, '"$2": '); 5 | return JSON.parse(details); 6 | } -------------------------------------------------------------------------------- /app/components/templates/Template.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Template extends Component { 4 | constructor(props){ 5 | super(props); 6 | this.state={ 7 | 8 | } 9 | } 10 | 11 | render() { 12 | return( 13 |

Hello world

14 | ) 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /webpack.config.node.js: -------------------------------------------------------------------------------- 1 | // for babel-plugin-webpack-loaders 2 | require('babel-register') 3 | const devConfigs = require('./webpack.config.dev') 4 | 5 | module.exports = { 6 | output: { 7 | libraryTarget: 'commonjs2' 8 | }, 9 | module: { 10 | loaders: devConfigs.module.loaders.slice(1) // remove babel-loader 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/templates/FunctionalTemplate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FunctionalTemplate = ({prop1,prop2}) => { 4 | const myFunc = () => alert("hi"); 5 | 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default FunctionalTemplate; -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /app/helpers/ObjectHelper.js: -------------------------------------------------------------------------------- 1 | export function subObject(object, startPropIndex, endPropIndex){ 2 | if(!object){ return null;} 3 | else if(startPropIndex==null){ return object; } 4 | 5 | let keys = Object.keys(object).slice(startPropIndex,endPropIndex); 6 | let newObj = {}; 7 | keys && keys.forEach(k=>{ 8 | newObj[k] = object[k]; 9 | }) 10 | return newObj; 11 | } -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | .tern-project 29 | .gitattributes 30 | .editorconfig 31 | .*ignore 32 | .eslintrc 33 | .jshintrc 34 | .flowconfig 35 | .documentup.json 36 | .yarn-metadata.json 37 | .*.yml 38 | *.yml 39 | 40 | # misc 41 | *.gz 42 | *.md 43 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Development - To Do 2 | 3 | ## Bugs 4 | * hangs when deleting many records 5 | 6 | ## Features 7 | ### General 8 | * error messages on bad query syntax 9 | * build commits into history, allow user to revert back to previous data 10 | * collapse sidebar 11 | 12 | ### Keymap 13 | * give shortcuts preview (ctrl+enter --> execute query, etc) 14 | * allow users to add shortcuts to paste saved queries 15 | 16 | ### Workbook 17 | * fix autocompletion, workbook should learn about common collections/props and use them as suggestions 18 | 19 | ### Query Translator 20 | * javascript first, then ios or android 21 | 22 | ## Later 23 | * implement ctrl-f : window.find like chrome -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["typecheck", "syntax-flow", "transform-flow-strip-types", "add-module-exports", "syntax-decorators", "transform-decorators-legacy"], 4 | "env": { 5 | "production": { 6 | "presets": ["react-optimize"], 7 | "plugins": [ 8 | "babel-plugin-dev-expression" 9 | ] 10 | }, 11 | "development": { 12 | "presets": ["react-hmre"] 13 | }, 14 | "test": { 15 | "plugins": [ 16 | ["webpack-loaders", { "config": "webpack.config.node.js", "verbose": false }], 17 | ["typecheck", {"disable": {"production": true}}] 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firestation Desktop Client 2 | Firestation is a Firebase admin tool that executes SQL queries against Firebase databases. 3 | 4 | 5 | [App Downloads](https://www.firestation.io/#download) 6 | 7 | ![](/select.gif) 8 | 9 | ## Up and Running 10 | 11 | * **Note: requires a node version >= 6 and an npm version >= 3.** 12 | 13 | ```bash 14 | git clone https://github.com/JoeRoddy/firestation.git 15 | cd firestation && npm install 16 | npm run dev 17 | ``` 18 | 19 | ### Firestore 20 | Firestore support is currently in development. To try it out: 21 | 22 | ```bash 23 | git clone https://github.com/JoeRoddy/firestation.git 24 | cd firestation 25 | git checkout projectRevamp 26 | npm install 27 | npm start 28 | ``` 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # OSX 30 | .DS_Store 31 | 32 | # App packaged 33 | dist 34 | release 35 | main.js 36 | main.js.map 37 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './components/App'; 4 | import './assets/stylesheets/base.scss'; 5 | import Store from './stores/Store'; 6 | import CacheHelper from './helpers/CacheHelper'; 7 | import { CACHE_RESET } from './config'; 8 | 9 | if (CACHE_RESET) { 10 | CacheHelper.updateLocalStore("databases", null); 11 | CacheHelper.updateLocalStore("currentDatabase", null); 12 | CacheHelper.updateLocalStore("savedQueriesByDb", null); 13 | CacheHelper.updateLocalStore("queryHistoryByDb", null); 14 | } 15 | 16 | const store = new Store(); 17 | const stores = { 18 | store: store 19 | }; 20 | 21 | render( 22 | , 23 | document.getElementById('root') 24 | ); 25 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default { 4 | module: { 5 | loaders: [{ 6 | test: /\.jsx?$/, 7 | loaders: ['babel-loader'], 8 | exclude: /node_modules/ 9 | }, { 10 | test: /\.json$/, 11 | loader: 'json-loader' 12 | }, 13 | { 14 | test: /\.s?css$/, 15 | loader: 'style!css!sass' 16 | } 17 | ] 18 | }, 19 | output: { 20 | path: path.join(__dirname, 'dist'), 21 | filename: 'bundle.js', 22 | libraryTarget: 'commonjs2' 23 | }, 24 | resolve: { 25 | // root:[], 26 | extensions: ['', '.js', '.jsx', '.json'], 27 | packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] 28 | }, 29 | plugins: [], 30 | externals: [] 31 | } 32 | -------------------------------------------------------------------------------- /app/components/modals/SaveQuery.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SaveQuery = ({ store }) => { 4 | const save = () => { 5 | if (!store.query) { 6 | store.modal = null; 7 | return; 8 | } 9 | const title = document.getElementById("new-query-name").value; 10 | const query = { title: title, body: store.query }; 11 | store.saveQuery(query); 12 | store.modal = null; 13 | } 14 | 15 | return ( 16 |
17 |

Save Query


18 | Give your query a name:   19 |


20 |    21 | 22 |
23 | ) 24 | } 25 | 26 | export default SaveQuery; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | import express from 'express' 4 | import webpack from 'webpack' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | 8 | import config from './webpack.config.dev' 9 | 10 | const app = express() 11 | const compiler = webpack(config) 12 | const PORT = process.env.PORT || 3000 13 | 14 | const devMiddleware = webpackDevMiddleware(compiler, { 15 | publicPath: config.output.publicPath, 16 | stats: { colors: true } 17 | }) 18 | 19 | app.use(devMiddleware) 20 | 21 | app.use(webpackHotMiddleware(compiler)) 22 | 23 | const server = app.listen(PORT, 'localhost', err => { 24 | if (err) return console.error(err) 25 | 26 | console.log(`Listening at http://localhost:${PORT}`) 27 | }); 28 | 29 | process.on('SIGTERM', () => { 30 | console.log('Stopping dev server') 31 | devMiddleware.close() 32 | server.close(() => process.exit(0)) 33 | }) 34 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import merge from 'webpack-merge' 3 | 4 | import baseConfig from './webpack.config' 5 | 6 | export default merge(baseConfig, { 7 | devtool: 'source-map', 8 | 9 | entry: ['babel-polyfill', './main.development'], 10 | 11 | output: { 12 | path: __dirname, 13 | filename: './main.js' 14 | }, 15 | 16 | plugins: [ 17 | new webpack.optimize.UglifyJsPlugin({ 18 | compressor: { 19 | warnings: false 20 | } 21 | }), 22 | new webpack.BannerPlugin( 23 | 'require("source-map-support").install();', 24 | { raw: true, entryOnly: false } 25 | ), 26 | new webpack.DefinePlugin({ 27 | 'process.env': { 28 | NODE_ENV: JSON.stringify('production') 29 | } 30 | }) 31 | ], 32 | 33 | target: 'electron-main', 34 | 35 | node: { 36 | __dirname: false, 37 | __filename: false 38 | }, 39 | 40 | externals: [ 41 | 'font-awesome', 42 | 'source-map-support' 43 | ] 44 | }) 45 | -------------------------------------------------------------------------------- /app/service/FirebaseService.js: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin'; 2 | 3 | export default class FirebaseService { 4 | static databaseConfigInitializes(db) { 5 | let testApp; 6 | try { 7 | testApp = admin.initializeApp({ 8 | credential: admin.credential.cert(db.serviceKey), 9 | databaseURL: db.url 10 | }, db.url); 11 | } catch (err) { 12 | return false; 13 | } 14 | 15 | testApp.delete(); 16 | return true; 17 | } 18 | 19 | static startFirebaseApp(db) { 20 | if (!db) { return null; } 21 | let apps = admin.apps; 22 | for (let i = 0; i < apps.length; i++) { 23 | if (apps[i].name === db.url) { 24 | return apps[i]; 25 | } 26 | } 27 | 28 | //app doesnt exist yet 29 | return admin.initializeApp({ 30 | credential: admin.credential.cert(db.serviceKey), 31 | databaseURL: db.url 32 | }, db.url); 33 | } 34 | } -------------------------------------------------------------------------------- /app/helpers/DateHelper.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 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 | -------------------------------------------------------------------------------- /app/service/UpdateService.js: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin'; 2 | 3 | export default class UpdateService { 4 | static updateFields(db, path, object, fields) { 5 | if (!fields || !object) { 6 | return; 7 | } 8 | var ref = db.ref(path); 9 | ref.once("value", function (snapshot) { 10 | let results = snapshot.val(); 11 | fields.forEach(field => { 12 | if(field.includes('/')){ 13 | let keyValSplit = field.split('/'); 14 | results[keyValSplit[0]] = results[keyValSplit[0]] || {}; 15 | results[keyValSplit[0]][keyValSplit[1]] = object[field]; 16 | }else { 17 | results[field] = object[field]; 18 | } 19 | }) 20 | return db.ref(path).update(results); 21 | }, function (errorObject) { 22 | console.log("UPDATE ERROR: " + errorObject.code); 23 | }); 24 | } 25 | 26 | static deleteObject(db, path) { 27 | db.ref(path).remove(); 28 | } 29 | 30 | static pushObject(db, path, object) { 31 | db.ref(path).push(object); 32 | } 33 | 34 | static set(db, path, object) { 35 | db.ref(path).set(object); 36 | } 37 | } -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | import webpack from 'webpack' 3 | import merge from 'webpack-merge' 4 | 5 | import baseConfig from './webpack.config' 6 | 7 | const port = process.env.PORT || 3000 8 | 9 | export default merge(baseConfig, { 10 | debug: true, 11 | 12 | devtool: 'cheap-module-eval-source-map', 13 | 14 | entry: [ 15 | `webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`, 16 | './app/index' 17 | ], 18 | 19 | output: { 20 | publicPath: `http://localhost:${port}/dist/` 21 | }, 22 | 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.global\.css$/, 27 | loaders: [ 28 | 'style-loader', 29 | 'css-loader?sourceMap' 30 | ] 31 | }, 32 | 33 | { 34 | test: /^((?!\.global).)*\.css$/, 35 | loaders: [ 36 | 'style-loader', 37 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 38 | ] 39 | } 40 | ] 41 | }, 42 | 43 | plugins: [ 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoErrorsPlugin(), 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify('development') 48 | }) 49 | ], 50 | 51 | target: 'electron-renderer' 52 | }); 53 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 3 | import merge from 'webpack-merge' 4 | import baseConfig from './webpack.config' 5 | 6 | const config = merge(baseConfig, { 7 | devtool: 'cheap-module-source-map', 8 | 9 | entry: './app/index', 10 | 11 | output: { 12 | publicPath: '../dist/' 13 | }, 14 | 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.global\.css$/, 19 | loader: ExtractTextPlugin.extract( 20 | 'style-loader', 21 | 'css-loader' 22 | ) 23 | }, 24 | 25 | { 26 | test: /^((?!\.global).)*\.css$/, 27 | loader: ExtractTextPlugin.extract( 28 | 'style-loader', 29 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 30 | ) 31 | } 32 | ] 33 | }, 34 | 35 | plugins: [ 36 | new webpack.optimize.OccurrenceOrderPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify('production') 39 | }), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compressor: { 42 | screw_ie8: true, 43 | warnings: false 44 | } 45 | }), 46 | new ExtractTextPlugin('style.css', { allChunks: true }) 47 | ], 48 | 49 | target: 'electron-renderer' 50 | }) 51 | 52 | export default config 53 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "rules": { 13 | "semi":0, 14 | "consistent-return": 0, 15 | "comma-dangle": 0, 16 | "no-use-before-define": 0, 17 | 18 | "import/no-unresolved": [2, { "ignore": ["electron"] }], 19 | "import/no-extraneous-dependencies": 0, 20 | 21 | "react/jsx-no-bind": 2, 22 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx"] }], 23 | "react/prefer-stateless-function": 2, 24 | "flowtype/define-flow-type": 1, 25 | "flowtype/require-parameter-type": [1,{"excludeArrowFunctions": "expressionsOnly"}], 26 | "flowtype/require-return-type": [1,"always",{"annotateUndefined": "never", "excludeArrowFunctions": "expressionsOnly"}], 27 | "flowtype/space-after-type-colon": [1,"always"], 28 | "flowtype/space-before-type-colon": [1,"never"], 29 | "flowtype/type-id-match": [1,"^([A-Z][a-z0-9]+)+Type$"], 30 | "flowtype/use-flow-type": 1, 31 | "flowtype/valid-syntax": 1, 32 | 33 | "prettier/prettier": "error" 34 | }, 35 | "plugins": [ 36 | "import", 37 | "react", 38 | "flowtype", 39 | "prettier" 40 | ], 41 | "settings": { 42 | "import/resolver": { 43 | "webpack": { 44 | "config": "webpack.config.eslint.js" 45 | } 46 | }, 47 | "flowtype": { 48 | "onlyFilesWithFlowAnnotation": false 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Firestation 7 | 15 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 |
27 | 36 | 38 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/components/object_tree/ObjectTree.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import PropTypes from "prop-types"; 4 | import ObjectNode from "./ObjectNode"; 5 | import { subObject } from "../../helpers/ObjectHelper"; 6 | /** 7 | * https://github.com/stomita/react-object-tree/ 8 | */ 9 | export default class ObjectTree extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.setPathUnderEdit = this.setPathUnderEdit.bind(this); 13 | this.setCreationPath = this.setCreationPath.bind(this); 14 | this.state = { 15 | pathUnderEdit: null, 16 | creationPath: null 17 | }; 18 | } 19 | 20 | setPathUnderEdit(pathUnderEdit) { 21 | this.setState({ pathUnderEdit, creationPath: null }); 22 | } 23 | 24 | setCreationPath(creationPath) { 25 | this.setState({ creationPath, pathUnderEdit: null }); 26 | } 27 | 28 | render() { 29 | const { className, value, level, noValue, store } = this.props; 30 | if (!value || value.payload == undefined) { 31 | return ; 32 | } 33 | //^ payload can be false 34 | 35 | const resultsToDisplayInTree = subObject(value.payload, 0, 50); 36 | const props = { 37 | value: resultsToDisplayInTree, 38 | path: "", 39 | pathUnderEdit: this.state.pathUnderEdit, 40 | setPathUnderEdit: this.setPathUnderEdit, 41 | creationPath: this.state.creationPath, 42 | setCreationPath: this.setCreationPath, 43 | fbPath: value.path, 44 | level: level, 45 | noValue: noValue, 46 | store: store 47 | }; 48 | 49 | return ( 50 |
51 |
52 | {this.props.resultsOpen && } 53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | ObjectTree.propTypes = { 60 | value: PropTypes.any.isRequired, 61 | level: PropTypes.number 62 | }; 63 | 64 | ObjectTree.defaultProps = { 65 | level: 0 66 | }; 67 | -------------------------------------------------------------------------------- /app/helpers/StringHelper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class StringHelper { 4 | static regexIndexOf(string, regex, startpos) { 5 | var indexOf = string.substring(startpos || 0).search(regex); 6 | return indexOf >= 0 ? indexOf + (startpos || 0) : indexOf; 7 | } 8 | 9 | static replaceAll(string, regex, replacement) { 10 | return string.replace(new RegExp(regex, "g"), replacement); 11 | } 12 | 13 | static replaceAllIgnoreCase(string, regex, replacement) { 14 | return string.replace(new RegExp(regex, "g", "i"), replacement); 15 | } 16 | 17 | static regexLastIndexOf(string, regex, startpos) { 18 | regex = regex.global 19 | ? regex 20 | : new RegExp( 21 | regex.source, 22 | "g" + (regex.ignoreCase ? "i" : "") + (regex.multiLine ? "m" : "") 23 | ); 24 | if (typeof startpos == "undefined") { 25 | startpos = this.length; 26 | } else if (startpos < 0) { 27 | startpos = 0; 28 | } 29 | var stringToWorkWith = string.substring(0, startpos + 1); 30 | var lastIndexOf = -1; 31 | var nextStop = 0; 32 | while ((result = regex.exec(stringToWorkWith)) != null) { 33 | lastIndexOf = result.index; 34 | regex.lastIndex = ++nextStop; 35 | } 36 | return lastIndexOf; 37 | } 38 | 39 | static getJsxWithNewLines(text) { 40 | return text.split("\n").map(function(item, key) { 41 | return ( 42 | 43 | {item} 44 |
45 |
46 | ); 47 | }); 48 | } 49 | 50 | static getParsedValue(stringVal, quotesMandatory) { 51 | if (!isNaN(stringVal)) { 52 | return parseFloat(stringVal); 53 | } else if (stringVal === "true" || stringVal === "false") { 54 | return stringVal === "true"; 55 | } else if (stringVal === "null") { 56 | return null; 57 | } else if (quotesMandatory) { 58 | stringVal = stringVal.trim(); 59 | if (stringVal.match(/^["|'].+["|']$/)) { 60 | return stringVal.replace(/["']/g, ""); 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 | -------------------------------------------------------------------------------- /app/components/modals/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AddDatabase from "./AddDatabase"; 3 | import DatabaseConfig from "./DatabaseConfig"; 4 | import SaveQuery from "./SaveQuery"; 5 | import fs from "fs"; 6 | const { dialog, app } = require("electron").remote; 7 | 8 | const Modal = ({ store, currentDatabase, createDb }) => { 9 | let serviceAccount = store.modal.includes("service") ? true : false; 10 | 11 | const handleFile = () => { 12 | dialog.showOpenDialog( 13 | { 14 | defaultPath: app.getPath("downloads"), 15 | filters: [{ name: "json", extensions: ["json"] }] 16 | }, 17 | fileNames => { 18 | console.log('filenames:',fileNames); 19 | if (fileNames === undefined) { 20 | console.log("No file selected"); 21 | return; 22 | } else if (fileNames.length > 1) { 23 | alert("Select only one file."); 24 | return; 25 | } 26 | fs.readFile(fileNames[0], "utf-8", (err, data) => { 27 | if (err) { 28 | alert("An error ocurred reading the file :" + err.message); 29 | return; 30 | } 31 | store.newDb = { path: fileNames[0], data: JSON.parse(data) }; 32 | }); 33 | } 34 | ); 35 | }; 36 | 37 | const closeModal = () => { 38 | store.modal = null; 39 | store.newDb = null; 40 | }; 41 | 42 | return ( 43 |
44 |
e.stopPropagation()}> 45 | 46 | {store.modal.includes("config") && 47 | } 54 | {store.modal.includes("newDB") && 55 | } 61 | {store.modal === "saveQuery" && } 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default Modal; 68 | -------------------------------------------------------------------------------- /app/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | 5 | @observer 6 | export default class Navbar extends Component { 7 | renderDatabases = () => { 8 | if (!this.props.store.databases) { return null; } 9 | return this.props.store.databases.map((db, index) => { 10 | return this.props.setCurrentDb(db)} key={index}>{db.title}; 11 | }) 12 | } 13 | 14 | getDatabaseJsx = () => { 15 | const { store } = this.props; 16 | if (!store.databases) { 17 | return
  • store.modal = "newDB"}>Add Your DB
  • 18 | } else { 19 | return ( 20 |
  • 21 | 25 |
    26 | store.modal = "newDB"}>Add New DB 27 |
    28 | {this.renderDatabases()} 29 |
    30 |
  • 31 | ) 32 | } 33 | } 34 | 35 | render() { 36 | return ( 37 | 47 | ) 48 | } 49 | } -------------------------------------------------------------------------------- /app/components/QueryResults.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactTooltip from "react-tooltip"; 4 | 5 | import { formatDate } from "../helpers/DateHelper"; 6 | import StringHelper from "../helpers/StringHelper"; 7 | import ObjectTree from "./object_tree/ObjectTree"; 8 | 9 | const QueryResults = props => { 10 | const store = props.store; 11 | return ( 12 |
    13 |
    14 |

    15 | {renderResultsTitle(props.payloadSize, store.results)} 16 |

    17 | 18 | {props.resultsOpen ? "Collapse results" : "Expand results"} 19 | 20 | { 29 | props.setWorkstationState("resultsOpen", !props.resultsOpen); 30 | }} 31 | /> 32 |
    33 | {props.payloadSize > 0 && 34 | props.store.results.payload != null && 35 | } 36 |
    37 | ); 38 | }; 39 | 40 | const renderResultsTitle = (payloadSize, results) => { 41 | let payloadDescription = 42 | payloadSize > 50 ? "Displaying 50 of " + payloadSize : payloadSize; 43 | switch (results.statementType) { 44 | case "UPDATE_STATEMENT": 45 | return ( 46 | 47 | Updated Records ({payloadDescription}): 48 | 49 | ); 50 | case "INSERT_STATEMENT": 51 | let numInserted = results.insertCount > 1 ? " (" + results.insertCount + "): " : ": "; 52 | return "Inserted Records" + numInserted; 53 | case "DELETE_STATEMENT": 54 | return ( 55 | 56 | Records to Delete ({payloadDescription}): 57 | 58 | ); 59 | default: 60 | return ( 61 | 62 | {results.path} ({payloadDescription}): 63 | 64 | ); 65 | } 66 | }; 67 | 68 | QueryResults.propTypes = { 69 | resultsOpen: PropTypes.bool, 70 | payloadSize: PropTypes.number, 71 | store: PropTypes.object.isRequired 72 | }; 73 | 74 | export default QueryResults; 75 | -------------------------------------------------------------------------------- /app/components/QueryHistory.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { formatDate } from "../helpers/DateHelper"; 4 | import StringHelper from "../helpers/StringHelper"; 5 | import ReactTooltip from "react-tooltip"; 6 | 7 | const QueryHistory = ({ history, store }) => { 8 | const queryTextLimit = 20; 9 | 10 | return ( 11 |
    12 |

    History

    13 |
    14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {history && 24 | history.map((query, i) => { 25 | return ( 26 | 27 | 30 | 40 | 43 | {query.body.length > queryTextLimit && 44 | 50 | 51 | {StringHelper.getJsxWithNewLines(query.body)} 52 | 53 | } 54 | 55 | ); 56 | })} 57 | 58 |
    DateQueryCommitted
    28 | {formatDate(query.date)} 29 | store.appendQuery(query.body)} 35 | > 36 | {query.body.length <= queryTextLimit 37 | ? query.body 38 | : query.body.substring(0, queryTextLimit - 3) + "..."} 39 | 41 | {query.committed && } 42 |
    59 |
    60 |
    61 | ); 62 | }; 63 | 64 | QueryHistory.propTypes = { 65 | history: PropTypes.object.isRequired, 66 | store: PropTypes.object.isRequired 67 | }; 68 | 69 | export default QueryHistory; 70 | -------------------------------------------------------------------------------- /app/components/ButtonRow.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactTooltip from "react-tooltip"; 4 | 5 | const ButtonRow = props => { 6 | const payloadSize = props.payloadSize; 7 | const store = props.store; 8 | return ( 9 |
    10 | {!store.commitQuery || !payloadSize 11 | ? 16 | :
    17 | 20 | 23 |
    } 24 |
    25 | {store.query && 26 |
    27 | 35 | 41 | Save Query 42 | 43 |
    } 44 | 52 | 58 | History 59 | 60 |
    61 |
    62 | ); 63 | }; 64 | 65 | ButtonRow.propTypes = { 66 | payloadSize: PropTypes.number, 67 | store: PropTypes.object.isRequired, 68 | execute: PropTypes.func.isRequired, 69 | commit: PropTypes.func.isRequired, 70 | cancelCommit: PropTypes.func.isRequired, 71 | saveQuery: PropTypes.func.isRequired, 72 | executingQuery: PropTypes.bool.isRequired 73 | }; 74 | 75 | export default ButtonRow; 76 | -------------------------------------------------------------------------------- /app/components/Workbook.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import AceEditor from "react-ace"; 3 | import brace from "brace"; 4 | import "brace/mode/sql"; 5 | import "brace/theme/github"; 6 | import "brace/ext/language_tools"; 7 | 8 | export default class Workbook extends Component { 9 | componentWillReceiveProps(nextProps) { 10 | // const langTools = ace.acequire('ace/ext/language_tools'); 11 | // const terms = ["SELECT", "UPDATE", "INSERT", "WHERE", "select"]; 12 | // var customCompleter = { 13 | // getCompletions: function (editor, session, pos, prefix, callback) { 14 | // if (prefix.length === 0) { callback(null, []); return } 15 | // callback(null, terms.map(term => { 16 | // return { name: term, value: term, score: 300, meta: "rhyme" } 17 | // })) 18 | // } 19 | // } 20 | // langTools.addCompleter(customCompleter); 21 | } 22 | 23 | componentDidUpdate() { 24 | //query inserted, move to end of workbook 25 | if (this.props.store && this.props.store.focus && this.refs.code) { 26 | this.refs.code.editor.focus(); 27 | this.refs.code.editor.navigateFileEnd(); 28 | this.props.store.focus = false; 29 | } 30 | } 31 | 32 | render() { 33 | const { execute, query, defaultValue, listenForCtrlEnter } = this.props; 34 | 35 | const store = this.props.store; 36 | if (!store) { 37 | return ; 38 | } 39 | 40 | let commands = [ 41 | { 42 | name: "execute", 43 | exec: execute, 44 | bindKey: { mac: "cmd-enter", win: "ctrl-enter" } 45 | } 46 | ]; 47 | 48 | let selectedTextChange = (newValue, e) => { 49 | store.selectedText = newValue; 50 | console.log("e:", e); 51 | console.log("selectedText:", newValue); 52 | }; 53 | 54 | return ( 55 | // add props enableBasicAutocompletion, enableLiveAutocompletion 56 | // to re-enable autocomplete 57 |
    58 | { 68 | store.query = e; 69 | }} 70 | defaultValue={defaultValue} 71 | value={store.query} 72 | name="UNIQUE_ID_OF_DIV" 73 | commands={commands} 74 | editorProps={{ $blockScrolling: true }} 75 | /> 76 |
    77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/components/modals/AddDatabase.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { convertJsonToDbConfig } from '../../helpers/JsonHelper'; 4 | import { shell } from 'electron'; 5 | 6 | const AddDatabase = ({ store, createDb, serviceAccount, handleFile }) => { 7 | const save = () => { 8 | let serviceKey = store.newDb.data; 9 | if(!serviceKey){ 10 | alert("Something went wrong with your file."); 11 | return; 12 | } 13 | let title = document.getElementById("db-title-input").value; 14 | title = title ? title : "My Firebase DB"; 15 | let path = store.newDb.path; 16 | path = path.substring(path.lastIndexOf("/")+1); 17 | const database = { 18 | title: title, 19 | serviceKey: serviceKey, 20 | url: "https://"+serviceKey.project_id+".firebaseio.com", 21 | path: path 22 | } 23 | 24 | const errMsg = createDb(database); 25 | if (errMsg) { 26 | alert(errMsg); 27 | } else{ 28 | store.newDb = null; 29 | } 30 | }; 31 | 32 | const clearNewDb = () => { 33 | store.newDb = null; 34 | } 35 | 36 | return ( 37 |
    38 |
    39 |

    Add a Firebase Database


    40 |
    41 |

    1) shell.openExternal('https://console.firebase.google.com/u/0/project/_/settings/serviceaccounts/adminsdk')}> 42 | Select your project on Firebase

    43 |

    2) Select "GENERATE NEW PRIVATE KEY"

    44 |

    3) Import the key into Firestation

    45 |
    46 | 48 | Note: this key never leaves your machine 49 |
    50 | {store.newDb && store.newDb.path && 51 |

    {store.newDb.path}
    52 | } 53 |
    54 |
    55 |

    56 |
    57 | 58 |
    59 |
    60 | ) 61 | } 62 | 63 | export default AddDatabase; -------------------------------------------------------------------------------- /app/components/modals/DatabaseConfig.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FirebaseService from '../../service/FirebaseService'; 3 | 4 | const DatabaseConfig = ({ store, handleFile, closeModal }) => { 5 | const currentDatabase = store.currentDatabase; 6 | const save = () => { 7 | let database = store.currentDatabase; 8 | let title = document.getElementById("db-title-input").value; 9 | title = title ? title : store.currentDatabase.title; 10 | database.title = title; 11 | if (!store.newDb || !store.newDb.data) { 12 | store.modal = null; 13 | store.updateDatabase(database); 14 | return; 15 | } 16 | 17 | let path = store.newDb.path; 18 | path = path.substring(path.lastIndexOf("/") + 1); 19 | database.serviceKey = serviceKey; 20 | database.url = "https://" + serviceKey.project_id + ".firebaseio.com"; 21 | database.path = path; 22 | let errMsg = FirebaseService.databaseConfigInitializes(database) ? 23 | null : "Something went wrong with your DB config file. It should look something like: myDatabaseName-firebase-adminsdk-4ieef-1521f1bc13.json"; 24 | if (errMsg) { 25 | alert(errMsg); 26 | } else { 27 | store.updateDatabase(database); 28 | store.modal = null; 29 | } 30 | } 31 | 32 | const clearNewDb = () => { 33 | store.newDb = {data:null}; 34 | } 35 | 36 | return ( 37 |
    38 |
    39 |

    DB: {currentDatabase.title}


    40 |
    41 |

    Name:

    42 |

    43 |
    44 |
    45 | 47 | {store.newDb && store.newDb.path ? 48 |
    New Service Account:
    {store.newDb.path}
    49 | : 50 |
    Current Service Account:
    {currentDatabase.path}
    51 | } 52 | 53 |

    54 | 55 | 56 |
    57 |
    58 | ) 59 | } 60 | 61 | export default DatabaseConfig; -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | // /* eslint strict: 0, no-shadow: 0, no-unused-vars: 0, no-console: 0 */ 3 | 4 | require('babel-polyfill') 5 | 6 | const os = require('os') 7 | const webpack = require('webpack') 8 | const electronCfg = require('./webpack.config.electron') 9 | const cfg = require('./webpack.config.prod') 10 | const packager = require('electron-packager') 11 | const del = require('del') 12 | const exec = require('child_process').exec 13 | const argv = require('minimist')(process.argv.slice(2)) 14 | const pkg = require('./package.json') 15 | 16 | const deps = Object.keys(pkg.dependencies) 17 | const devDeps = Object.keys(pkg.devDependencies) 18 | 19 | const appName = argv.name || argv.n || pkg.productName 20 | const shouldUseAsar = argv.asar || argv.a || false 21 | const shouldBuildAll = argv.all || false 22 | 23 | 24 | const DEFAULT_OPTS = { 25 | dir: './', 26 | name: appName, 27 | asar: shouldUseAsar, 28 | ignore: [ 29 | '^/test($|/)', 30 | '^/release($|/)', 31 | '^/main.development.js' 32 | ].concat(devDeps.map(name => `/node_modules/${name}($|/)`)) 33 | .concat( 34 | deps.filter(name => !electronCfg.externals.includes(name)) 35 | .map(name => `/node_modules/${name}($|/)`) 36 | ) 37 | } 38 | 39 | const icon = argv.icon || argv.i || './icon.icns' 40 | 41 | if (icon) { 42 | DEFAULT_OPTS.icon = icon; 43 | } 44 | 45 | const version = argv.version || argv.v 46 | 47 | if (version) { 48 | DEFAULT_OPTS.version = version; 49 | startPack() 50 | } else { 51 | // use the same version as the currently-installed electron-prebuilt 52 | exec('npm list electron-prebuilt --dev', (err, stdout) => { 53 | if (err) { 54 | DEFAULT_OPTS.version = '1.2.0'; 55 | } else { 56 | DEFAULT_OPTS.version = stdout.split('electron-prebuilt@')[1].replace(/\s/g, '') 57 | } 58 | 59 | startPack() 60 | }) 61 | } 62 | 63 | 64 | function build(config) { 65 | return new Promise((resolve, reject) => { 66 | webpack(config, (err, stats) => { 67 | if (err) return reject(err) 68 | resolve(stats) 69 | }) 70 | }) 71 | } 72 | 73 | function startPack() { 74 | console.log('start pack...') 75 | build(electronCfg) 76 | .then(() => build(cfg)) 77 | .then(() => del('release')) 78 | .then(() => { 79 | if (shouldBuildAll) { 80 | // build for all platforms 81 | const archs = ['ia32', 'x64']; 82 | const platforms = ['linux', 'win32', 'darwin']; 83 | 84 | platforms.forEach(plat => { 85 | archs.forEach(arch => { 86 | pack(plat, arch, log(plat, arch)) 87 | }) 88 | }) 89 | } else { 90 | // build for current platform only 91 | pack(os.platform(), os.arch(), log(os.platform(), os.arch())) 92 | } 93 | }) 94 | .catch(err => { 95 | console.error(err) 96 | }) 97 | } 98 | 99 | function pack(plat, arch, cb) { 100 | // there is no darwin ia32 electron 101 | if (plat === 'darwin' && arch === 'ia32') return; 102 | 103 | const iconObj = { 104 | icon: DEFAULT_OPTS.icon + (() => { 105 | let extension = '.png'; 106 | if (plat === 'darwin') { 107 | extension = '.icns'; 108 | } else if (plat === 'win32') { 109 | extension = '.ico'; 110 | } 111 | return extension; 112 | })() 113 | }; 114 | 115 | const opts = Object.assign({}, DEFAULT_OPTS, iconObj, { 116 | platform: plat, 117 | arch, 118 | prune: true, 119 | 'app-version': pkg.version || DEFAULT_OPTS.version, 120 | out: `release/${plat}-${arch}` 121 | }) 122 | 123 | packager(opts, cb) 124 | } 125 | 126 | 127 | function log(plat, arch) { 128 | return err => { 129 | if (err) return console.error(err) 130 | console.log(`${plat}-${arch} finished!`) 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firestation-electron", 3 | "version": "0.1.0", 4 | "description": "Execute SQL Queries Against Your Firestation Database", 5 | "productName": "Firestation", 6 | "main": "main.js", 7 | "scripts": { 8 | "lint": "eslint app test *.js", 9 | "hot-server": "node -r babel-register server.js", 10 | "build-main": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors", 11 | "build-renderer": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.prod.js --progress --profile --colors", 12 | "build": "npm run build-main && npm run build-renderer", 13 | "start": "cross-env NODE_ENV=production electron ./", 14 | "start-hot": "cross-env HOT=1 NODE_ENV=development electron -r babel-register -r babel-polyfill ./main.development", 15 | "package": "cross-env NODE_ENV=production node -r babel-register package.js", 16 | "package-all": "npm run package -- --all", 17 | "dev": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\"" 18 | }, 19 | "bin": { 20 | "electron": "./node_modules/.bin/electron" 21 | }, 22 | "license": "MIT", 23 | "devDependencies": { 24 | "asar": "^0.12.3", 25 | "babel-core": "^6.14.0", 26 | "babel-eslint": "^6.1.2", 27 | "babel-loader": "^6.2.5", 28 | "babel-plugin-add-module-exports": "^0.2.1", 29 | "babel-plugin-dev-expression": "^0.2.1", 30 | "babel-plugin-syntax-decorators": "^6.13.0", 31 | "babel-plugin-syntax-flow": "^6.13.0", 32 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 33 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 34 | "babel-plugin-typecheck": "^3.9.0", 35 | "babel-plugin-webpack-loaders": "^0.7.1", 36 | "babel-polyfill": "^6.13.0", 37 | "babel-preset-es2015": "^6.14.0", 38 | "babel-preset-react": "^6.11.1", 39 | "babel-preset-react-hmre": "^1.1.1", 40 | "babel-preset-react-optimize": "^1.0.1", 41 | "babel-preset-stage-0": "^6.5.0", 42 | "babel-register": "^6.14.0", 43 | "concurrently": "^2.2.0", 44 | "cross-env": "^2.0.1", 45 | "css-loader": "^0.26.1", 46 | "del": "^2.2.2", 47 | "devtron": "^1.3.0", 48 | "electron": "^1.8.8", 49 | "electron-devtools-installer": "^2.0.1", 50 | "electron-packager": "^7.7.0", 51 | "electron-rebuild": "^1.2.0", 52 | "electron-remote": "^1.0.8", 53 | "eslint": "^4.18.2", 54 | "eslint-config-airbnb": "^10.0.1", 55 | "eslint-import-resolver-webpack": "^0.5.1", 56 | "eslint-plugin-flowtype": "^2.11.4", 57 | "eslint-plugin-import": "^1.14.0", 58 | "eslint-plugin-jsx-a11y": "^2.2.0", 59 | "eslint-plugin-prettier": "^2.3.1", 60 | "eslint-plugin-react": "^6.2.0", 61 | "express": "^4.14.0", 62 | "extract-text-webpack-plugin": "^1.0.1", 63 | "fbjs-scripts": "^0.7.1", 64 | "flow": "^0.2.3", 65 | "flow-bin": "^0.31.1", 66 | "json-loader": "^0.5.4", 67 | "minimist": "^1.2.0", 68 | "node-libs-browser": "^1.0.0", 69 | "node-sass": "^3.4.2", 70 | "prettier": "^1.5.2", 71 | "react-addons-test-utils": "^15.3.1", 72 | "sass-loader": "^3.2.0", 73 | "spectron": "^3.3.0", 74 | "style-loader": "^0.13.1", 75 | "webpack": "^1.13.2", 76 | "webpack-dev-middleware": "^1.6.1", 77 | "webpack-hot-middleware": "^2.12.2", 78 | "webpack-merge": "^0.14.1" 79 | }, 80 | "dependencies": { 81 | "aws-sdk": "^2.154.0", 82 | "electron-debug": "^1.0.1", 83 | "firebase": "^4.6.2", 84 | "firebase-admin": "^5.5.0", 85 | "jquery": "^3.4.0", 86 | "lodash": "^4.17.13", 87 | "mobx": "^2.4.4", 88 | "mobx-react": "^3.5.5", 89 | "mobx-react-devtools": "^4.2.5", 90 | "moment": "^2.19.3", 91 | "react": "^15.3.1", 92 | "react-ace": "^4.3.0", 93 | "react-dom": "^15.3.1", 94 | "react-tooltip": "^3.3.0", 95 | "source-map-support": "^0.4.2", 96 | "type-name": "^2.0.2" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/components/SideMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FirebaseService from "../service/FirebaseService"; 3 | import fs from "fs"; 4 | import moment from "moment"; 5 | const { dialog, app } = require("electron").remote; 6 | const shell = require("electron").shell; 7 | 8 | const SideMenu = ({ 9 | savedQueries, 10 | deleteQuery, 11 | savedQueriesIsOpen, 12 | toggleSavedQueries, 13 | store 14 | }) => { 15 | const del = (e, query) => { 16 | e.stopPropagation(); 17 | let queryDescrip = query.title 18 | ? query.title 19 | : query.queryDescrip.substring(0, 100); 20 | if ( 21 | confirm( 22 | "Delete Query: " + 23 | queryDescrip + 24 | "\nThis will delete this query permanently, are you sure?" 25 | ) 26 | ) { 27 | store.deleteQuery(query.body); 28 | } 29 | }; 30 | 31 | const renderSavedQueries = () => { 32 | return savedQueries.map((query, index) => { 33 | return ( 34 |
    store.appendQuery(query.body)} 38 | > 39 | {query && query.title && query.title.substring(0, 22)} 40 | del(e, query)} /> 41 |
    42 | ); 43 | }); 44 | }; 45 | 46 | const savedCaret = () => { 47 | if (!savedQueries) { 48 | return null; 49 | } 50 | return savedQueriesIsOpen 51 | ? 52 | : ; 53 | }; 54 | 55 | const downloadBackup = () => { 56 | let db = FirebaseService.startFirebaseApp(store.currentDatabase).database(); 57 | let path = 58 | app.getPath("desktop") + 59 | "/" + 60 | moment().format("MMMDo_") + 61 | store.currentDatabase.title + 62 | ".json"; 63 | db.ref("/").once("value", snap => { 64 | let dbContent = snap.val(); 65 | dialog.showSaveDialog({ defaultPath: path }, fileName => { 66 | if (fileName === undefined) return; 67 | fs.writeFile(fileName, JSON.stringify(dbContent), function(err) {}); 68 | }); 69 | }); 70 | }; 71 | 72 | let projectId = 73 | store.currentDatabase && 74 | store.currentDatabase.serviceKey && 75 | store.currentDatabase.serviceKey.project_id; 76 | const firebaseLink = 77 | "https://console.firebase.google.com/" + 78 | (projectId ? `project/${projectId}/overview` : ""); 79 | 80 | return ( 81 |
    82 | (store.modal = "config")}> 83 |  DB Config 84 | 85 | {savedQueries && 86 | savedQueries.length > 0 && 87 | 88 |  Saved Queries {savedCaret()} 89 | } 90 | {savedQueries && 91 | savedQueries.length > 0 && 92 | savedQueriesIsOpen && 93 |
    94 | {renderSavedQueries()} 95 |
    } 96 | {/* Query Translator*/} 97 | 98 |  Download Backup 99 | 100 | shell.openExternal("https://docs.firestation.io/")} 103 | > 104 |  Documentation 105 | 106 | shell.openExternal(firebaseLink)} 109 | > 110 | 115 |  Firebase Console 116 | 117 |
    118 | ); 119 | }; 120 | 121 | export default SideMenu; 122 | -------------------------------------------------------------------------------- /app/components/Workstation.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ReactTooltip from "react-tooltip"; 3 | import { observer } from "mobx-react"; 4 | 5 | import Workbook from "./Workbook"; 6 | import SideMenu from "./SideMenu"; 7 | import QueryHistory from "./QueryHistory"; 8 | import QueryResults from "./QueryResults"; 9 | import ButtonRow from "./ButtonRow"; 10 | 11 | @observer 12 | export default class Workstation extends Component { 13 | state = { 14 | savedQueries: null, 15 | savedQueriesIsOpen: true, 16 | modal: null, 17 | resultsOpen: true 18 | }; 19 | 20 | componentDidMount() { 21 | if (!this.props.store.databases[0]) { 22 | this.props.store.modal = "newDB"; 23 | } 24 | } 25 | 26 | execute = () => { 27 | let selectedText = this.getSelectionText(); 28 | let query = this.props.store.query; 29 | if (selectedText && query.includes(selectedText)) { 30 | query = selectedText; 31 | } 32 | this.props.executeQuery(query); 33 | }; 34 | 35 | saveQuery = () => { 36 | this.props.store.modal = "saveQuery"; 37 | }; 38 | 39 | deleteQuery = query => { 40 | this.props.store.deleteQuery; 41 | }; 42 | 43 | toggleSavedQueries = () => { 44 | this.setState({ savedQueriesIsOpen: !this.state.savedQueriesIsOpen }); 45 | }; 46 | 47 | getSelectionText = () => { 48 | var text = ""; 49 | var activeEl = document.activeElement; 50 | var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null; 51 | if ( 52 | activeElTagName == "textarea" && 53 | typeof activeEl.selectionStart == "number" 54 | ) { 55 | text = activeEl.value.slice( 56 | activeEl.selectionStart, 57 | activeEl.selectionEnd 58 | ); 59 | } else if (window.getSelection) { 60 | text = window.getSelection().toString(); 61 | } 62 | 63 | return this.props.store.selectedText; 64 | }; 65 | 66 | setWorkstationState = (key, val) => { 67 | this.setState({ [key]: val }); 68 | }; 69 | 70 | render() { 71 | const store = this.props.store; 72 | const query = store.query; //updates children 73 | if (!store.databases[0]) { 74 | return ; 75 | } 76 | let payloadSize; 77 | if (store.results && !store.results.error) { 78 | if (store.results.payload === Object(store.results.payload)) { 79 | payloadSize = Object.keys(store.results.payload).length; 80 | } else if (store.results.payload === null) { 81 | payloadSize = 0; 82 | } else { 83 | //primitive payload 84 | payloadSize = 1; 85 | } 86 | } 87 | 88 | const props = { 89 | store, 90 | payloadSize, 91 | execute: this.execute, 92 | resultsOpen: this.state.resultsOpen, 93 | setWorkstationState: this.setWorkstationState 94 | }; 95 | 96 | return ( 97 |
    98 | 105 |
    106 |

    107 | {store.currentDatabase.title} 108 |

    109 | {/*{store.rootKeys && 110 |
    Root Keys:
    }*/} 112 | 113 | 120 |
    121 |
    128 | {store.results && 129 | store.results.error && 130 |

    131 | {store.results.error} 132 |

    } 133 | {store.results && 134 | payloadSize !== undefined && 135 | } 136 | {store.queryHistoryIsOpen && 137 | } 138 |
    139 |
    140 |
    141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { inject, observer } from "mobx-react"; 3 | import "../assets/stylesheets/base.scss"; 4 | import FirebaseService from "../service/FirebaseService"; 5 | import QueryHelper from "../helpers/QueryHelper"; 6 | import Workstation from "./Workstation"; 7 | import Navbar from "./Navbar"; 8 | import Modal from "./modals/Modal"; 9 | 10 | @observer 11 | export default class App extends Component { 12 | componentWillMount() { 13 | this.setCurrentDb(this.props.store.currentDatabase); 14 | } 15 | 16 | setCurrentDb = currentDatabase => { 17 | if (!currentDatabase) { 18 | return; 19 | } 20 | this.killFirebaseListeners(); 21 | FirebaseService.startFirebaseApp(currentDatabase); 22 | this.props.store.setCurrentDatabase(currentDatabase); 23 | // QueryHelper.getRootKeysPromise(currentDatabase).then(rootKeys => { 24 | // console.log(rootKeys) 25 | // this.props.store.rootKeys = rootKeys; 26 | // }) 27 | }; 28 | 29 | updateSavedQueries = db => { 30 | const dbUrl = db.config.databaseURL; 31 | let queriesByDb = this.props.store.savedQueriesByDb; 32 | let savedQueries = 33 | !queriesByDb || !queriesByDb[url] ? null : queriesByDb[url]; 34 | this.setState({ savedQueries }); 35 | }; 36 | 37 | createDb = database => { 38 | let err = this.props.store.createNewDatabase(database); 39 | if (err) { 40 | return err; 41 | } 42 | this.setCurrentDb(database); 43 | this.props.store.currentDatabase = database; 44 | this.props.store.modal = null; 45 | }; 46 | 47 | startFirebaseForDb = db => { 48 | FirebaseService.startFirebaseApp(db.url); 49 | }; 50 | 51 | executeQuery = query => { 52 | this.killFirebaseListeners(); 53 | query = QueryHelper.formatAndCleanQuery(query); 54 | this.props.store.addQueryToHistory(query); 55 | this.props.store.executingQuery = true; 56 | try { 57 | QueryHelper.executeQuery( 58 | query, 59 | this.props.store.currentDatabase, 60 | results => { 61 | this.props.store.executingQuery = false; 62 | if (results && results.queryType != "SELECT_STATEMENT") { 63 | this.props.store.commitQuery = query; 64 | this.props.store.results = results; 65 | this.props.store.firebaseListeners.push(results.firebaseListener); 66 | } else { 67 | this.props.store.results = results; 68 | this.props.store.firebaseListeners.push(results.firebaseListener); 69 | } 70 | } 71 | ); 72 | } catch (error) { 73 | this.props.store.results = { error }; 74 | this.props.store.executingQuery = false; 75 | } 76 | }; 77 | 78 | commit = () => { 79 | this.killFirebaseListeners(); 80 | if (!this.props.store.commitQuery || !this.props.store.currentDatabase) { 81 | return; 82 | } 83 | const query = QueryHelper.formatAndCleanQuery(this.props.store.commitQuery); 84 | this.props.store.markQueryAsCommitted(query); 85 | try { 86 | QueryHelper.executeQuery( 87 | query, 88 | this.props.store.currentDatabase, 89 | results => { 90 | this.props.store.firebaseListeners.push(results.firebaseListener); 91 | this.killFirebaseListeners(); 92 | this.props.store.clearResults(); 93 | }, 94 | true 95 | ); 96 | } catch (error) { 97 | this.props.store.results = { error }; 98 | } 99 | }; 100 | 101 | killFirebaseListeners = () => { 102 | this.props.store.firebaseListeners.forEach(ref => { 103 | ref && ref.off("value"); 104 | }); 105 | this.props.store.firebaseListeners = []; 106 | }; 107 | 108 | cancelCommit = () => { 109 | this.props.store.clearResults(); 110 | }; 111 | 112 | render() { 113 | console.log("store:", this.props.store); 114 | const savedQueries = 115 | this.props.store.savedQueriesByDb && this.props.store.currentDatabase 116 | ? this.props.store.savedQueriesByDb[ 117 | this.props.store.currentDatabase.url 118 | ] 119 | : null; 120 | 121 | const props = { 122 | cancelCommit: this.cancelCommit, 123 | createDb: this.createDb, 124 | commit: this.commit, 125 | executeQuery: this.executeQuery, 126 | results: this.props.store.results, 127 | newDb: this.props.store.newDb, 128 | savedQueries: savedQueries, 129 | setCurrentDb: this.setCurrentDb, 130 | startFirebaseForDb: this.startFirebaseForDb, 131 | store: this.props.store, 132 | updateSavedQueries: this.updateSavedQueries 133 | }; 134 | 135 | return ( 136 |
    137 | 138 | {this.props.store.modal && } 139 | 140 |
    141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/stores/Store.js: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | import CacheHelper from "../helpers/CacheHelper"; 3 | import FirebaseService from "../service/FirebaseService"; 4 | 5 | class Store { 6 | @observable databases = CacheHelper.getFromLocalStore("databases"); 7 | databases = this.databases ? this.databases : []; 8 | @observable 9 | currentDatabase = CacheHelper.getFromLocalStore("currentDatabase"); 10 | @observable rootKeys = null; 11 | @observable 12 | savedQueriesByDb = CacheHelper.getFromLocalStore("savedQueriesByDb"); 13 | @observable results = null; 14 | @observable commitQuery = null; 15 | @observable modal = null; 16 | @observable 17 | queryHistoryByDb = CacheHelper.getFromLocalStore("queryHistoryByDb"); 18 | @observable firebaseListeners = []; 19 | 20 | //Modals 21 | @observable newDb = { data: null }; 22 | 23 | //Workstation 24 | @observable queryHistoryIsOpen = false; 25 | @observable query = ""; 26 | @observable executingQuery = false; 27 | 28 | //Workbook 29 | @observable focus = false; 30 | @observable selectedText = ""; 31 | constructor() {} 32 | 33 | appendQuery(text) { 34 | const query = this.query ? this.query + "\n" + text : text; 35 | this.query = query; 36 | this.focus = true; 37 | } 38 | 39 | getQueryHistory() { 40 | if (!this.currentDatabase || !this.queryHistoryByDb) { 41 | return null; 42 | } 43 | return this.queryHistoryByDb[this.currentDatabase.url]; 44 | } 45 | 46 | addQueryToHistory(query) { 47 | if (!this.currentDatabase) { 48 | return; 49 | } 50 | const dbURL = this.currentDatabase.url; 51 | let queryHistoryByDb = this.queryHistoryByDb ? this.queryHistoryByDb : {}; 52 | let history = 53 | Object.keys(queryHistoryByDb).length > 0 && queryHistoryByDb[dbURL] 54 | ? queryHistoryByDb[dbURL] 55 | : []; 56 | let queryObj = { body: query.trim(), date: new Date() }; 57 | if (history && history.length >= 15) { 58 | history = history.slice(0, 14); 59 | } 60 | history.unshift(queryObj); 61 | 62 | queryHistoryByDb[dbURL] = history; 63 | this.queryHistoryByDb = queryHistoryByDb; 64 | CacheHelper.updateLocalStore("queryHistoryByDb", queryHistoryByDb); 65 | } 66 | 67 | markQueryAsCommitted(query) { 68 | try { 69 | let history = this.queryHistoryByDb[this.currentDatabase.url]; 70 | if (history[0].body.trim() !== query.trim()) { 71 | return; 72 | } 73 | history[0].committed = true; 74 | this.queryHistoryByDb[this.currentDatabase.url] = history; 75 | CacheHelper.updateLocalStore("queryHistoryByDb", this.queryHistoryByDb); 76 | } catch (err) { 77 | console.log(err); 78 | } 79 | } 80 | 81 | clearResults() { 82 | this.commitQuery = null; 83 | this.results = null; 84 | } 85 | 86 | setCurrentDatabase(database) { 87 | this.currentDatabase = database; 88 | this.queryHistoryIsOpen = false; 89 | this.query = ""; 90 | this.clearResults(); 91 | CacheHelper.updateLocalStore("currentDatabase", database); 92 | } 93 | 94 | createNewDatabase(database) { 95 | let err = this.checkDbForErrors(database); 96 | if (err) { 97 | return err; 98 | } 99 | let databases = this.databases; 100 | this.databases.push(database); 101 | this.currentDatabase = database; 102 | CacheHelper.updateLocalStore("databases", databases); 103 | CacheHelper.updateLocalStore("currentDatabase", database); 104 | let exampleQueries = this.getExampleQueries(); 105 | exampleQueries.forEach(q => { 106 | this.saveQuery(q); 107 | }); 108 | } 109 | 110 | updateDatabase(database) { 111 | let databases = this.databases.map(db => { 112 | if (database.serviceKey.project_id === db.serviceKey.project_id) { 113 | return database; 114 | } else { 115 | return db; 116 | } 117 | }); 118 | this.databases = databases; 119 | this.currentDatabase = database; 120 | CacheHelper.updateLocalStore("currentDatabase", database); 121 | CacheHelper.updateLocalStore("databases", databases); 122 | } 123 | 124 | checkDbForErrors(database) { 125 | let databases = this.databases; 126 | databases = databases ? databases : []; 127 | for (let i = 0; i < databases.length; i++) { 128 | let db = databases[i]; 129 | if (db.title === database.title) { 130 | return 'You already have a database with the name "' + db.title + '".'; 131 | } else if (db.serviceKey.project_id === database.serviceKey.project_id) { 132 | return 'This DB already exists as "' + db.title + '"'; 133 | } 134 | } 135 | if (!FirebaseService.databaseConfigInitializes(database)) { 136 | return "Something went wrong with your file. It should look something like: myDatabaseName-firebase-adminsdk-4ieef-1521f1bc13.json"; 137 | } 138 | return false; 139 | } 140 | 141 | saveQuery(query) { 142 | const url = this.currentDatabase.url; 143 | let queriesByDb = CacheHelper.getFromLocalStore("savedQueriesByDb"); 144 | queriesByDb = queriesByDb ? queriesByDb : {}; 145 | let queriesForThisDb = 146 | queriesByDb && queriesByDb[url] ? queriesByDb[url] : []; 147 | queriesForThisDb.push(query); 148 | queriesByDb[url] = queriesForThisDb; 149 | this.savedQueriesByDb = queriesByDb; 150 | CacheHelper.updateLocalStore("savedQueriesByDb", queriesByDb); 151 | } 152 | 153 | deleteQuery(query) { 154 | const url = this.currentDatabase.url; 155 | let queriesByDb = CacheHelper.getFromLocalStore("savedQueriesByDb"); 156 | queriesByDb = queriesByDb ? queriesByDb : {}; 157 | let queriesForThisDb = 158 | queriesByDb && queriesByDb[url] ? queriesByDb[url] : []; 159 | var i = queriesForThisDb.length; 160 | while (i--) { 161 | if (queriesForThisDb[i].body === query) { 162 | queriesForThisDb.splice(i, 1); 163 | } 164 | } 165 | queriesByDb[url] = queriesForThisDb; 166 | this.savedQueriesByDb = queriesByDb; 167 | CacheHelper.updateLocalStore("savedQueriesByDb", queriesByDb); 168 | } 169 | 170 | getExampleQueries() { 171 | return [ 172 | { 173 | title: "Example Select", 174 | body: "select * from users where email = 'johndoe@gmail.com';" 175 | }, 176 | { 177 | title: "Example Update", 178 | body: "update users set legendaryPlayer = true where level > 100;" 179 | }, 180 | { 181 | title: "Example Delete", 182 | body: "delete from users where cheater = true;" 183 | }, 184 | { 185 | title: "Example Insert", 186 | body: 187 | "insert into users (name, level, email) values ('Joe', 99, 'joe@gmail.com');" 188 | } 189 | ]; 190 | } 191 | } 192 | 193 | export default Store; 194 | -------------------------------------------------------------------------------- /main.development.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, shell } from 'electron' 2 | import path from 'path'; 3 | 4 | let menu 5 | let template 6 | let mainWindow = null 7 | 8 | 9 | if (process.env.NODE_ENV === 'development') { 10 | require('electron-debug')() // eslint-disable-line global-require 11 | } 12 | 13 | 14 | app.on('window-all-closed', () => { 15 | if (process.platform !== 'darwin') app.quit() 16 | }); 17 | 18 | 19 | const installExtensions = async () => { 20 | if (process.env.NODE_ENV === 'development') { 21 | const installer = require('electron-devtools-installer') // eslint-disable-line global-require 22 | 23 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REACT_PERF'] 24 | 25 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS 26 | for (const name of extensions) { 27 | try { 28 | await installer.default(installer[name], forceDownload); 29 | } catch (e) { } // eslint-disable-line 30 | } 31 | } 32 | } 33 | 34 | app.on('ready', async () => { 35 | await installExtensions() 36 | 37 | mainWindow = new BrowserWindow({ 38 | show: false, 39 | height: 800, 40 | width: 1281, 41 | icon: path.join(__dirname, 'app/assets/images/logo.ico'), 42 | title:"Firestation" 43 | 44 | }) 45 | 46 | mainWindow.loadURL(`file://${__dirname}/app/index.html`) 47 | 48 | mainWindow.webContents.on('did-finish-load', () => { 49 | mainWindow.show() 50 | mainWindow.focus() 51 | }); 52 | 53 | mainWindow.on('closed', () => { 54 | mainWindow = null 55 | }) 56 | 57 | if (process.env.NODE_ENV === 'development') { 58 | mainWindow.webContents.on('context-menu', (e, props) => { 59 | const { x, y } = props 60 | 61 | Menu.buildFromTemplate([{ 62 | label: 'Inspect element', 63 | click() { 64 | mainWindow.inspectElement(x, y) 65 | } 66 | }]).popup(mainWindow) 67 | }) 68 | } 69 | 70 | if (process.platform === 'darwin') { 71 | template = [{ 72 | label: 'Electron', 73 | submenu: [{ 74 | label: 'About ElectronReact', 75 | selector: 'orderFrontStandardAboutPanel:' 76 | }, { 77 | type: 'separator' 78 | }, { 79 | label: 'Services', 80 | submenu: [] 81 | }, { 82 | type: 'separator' 83 | }, { 84 | label: 'Hide ElectronReact', 85 | accelerator: 'Command+H', 86 | selector: 'hide:' 87 | }, { 88 | label: 'Hide Others', 89 | accelerator: 'Command+Shift+H', 90 | selector: 'hideOtherApplications:' 91 | }, { 92 | label: 'Show All', 93 | selector: 'unhideAllApplications:' 94 | }, { 95 | type: 'separator' 96 | }, { 97 | label: 'Quit', 98 | accelerator: 'Command+Q', 99 | click() { 100 | app.quit() 101 | } 102 | }] 103 | }, { 104 | label: 'Edit', 105 | submenu: [{ 106 | label: 'Undo', 107 | accelerator: 'Command+Z', 108 | selector: 'undo:' 109 | }, { 110 | label: 'Redo', 111 | accelerator: 'Shift+Command+Z', 112 | selector: 'redo:' 113 | }, { 114 | type: 'separator' 115 | }, { 116 | label: 'Cut', 117 | accelerator: 'Command+X', 118 | selector: 'cut:' 119 | }, { 120 | label: 'Copy', 121 | accelerator: 'Command+C', 122 | selector: 'copy:' 123 | }, { 124 | label: 'Paste', 125 | accelerator: 'Command+V', 126 | selector: 'paste:' 127 | }, { 128 | label: 'Select All', 129 | accelerator: 'Command+A', 130 | selector: 'selectAll:' 131 | }] 132 | }, { 133 | label: 'View', 134 | submenu: (process.env.NODE_ENV === 'development') ? [{ 135 | label: 'Reload', 136 | accelerator: 'Command+R', 137 | click() { 138 | mainWindow.webContents.reload() 139 | } 140 | }, { 141 | label: 'Toggle Full Screen', 142 | accelerator: 'Ctrl+Command+F', 143 | click() { 144 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 145 | } 146 | }, { 147 | label: 'Toggle Developer Tools', 148 | accelerator: 'Alt+Command+I', 149 | click() { 150 | mainWindow.toggleDevTools() 151 | } 152 | }] : [{ 153 | label: 'Toggle Full Screen', 154 | accelerator: 'Ctrl+Command+F', 155 | click() { 156 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 157 | } 158 | }] 159 | }, { 160 | label: 'Window', 161 | submenu: [{ 162 | label: 'Minimize', 163 | accelerator: 'Command+M', 164 | selector: 'performMiniaturize:' 165 | }, { 166 | label: 'Close', 167 | accelerator: 'Command+W', 168 | selector: 'performClose:' 169 | }, { 170 | type: 'separator' 171 | }, { 172 | label: 'Bring All to Front', 173 | selector: 'arrangeInFront:' 174 | }] 175 | }, { 176 | label: 'Help', 177 | submenu: [{ 178 | label: 'Learn More', 179 | click() { 180 | shell.openExternal('https://www.firestation.io') 181 | } 182 | }, { 183 | label: 'Documentation', 184 | click() { 185 | shell.openExternal('https://docs.firestation.io') 186 | } 187 | }, { 188 | label: 'View the Code', 189 | click() { 190 | shell.openExternal('https://github.com/JoeRoddy/firestation') 191 | } 192 | }] 193 | }]; 194 | 195 | menu = Menu.buildFromTemplate(template) 196 | Menu.setApplicationMenu(menu) 197 | } else { 198 | template = [{ 199 | label: '&File', 200 | submenu: [{ 201 | label: '&Open', 202 | accelerator: 'Ctrl+O' 203 | }, { 204 | label: '&Close', 205 | accelerator: 'Ctrl+W', 206 | click() { 207 | mainWindow.close() 208 | } 209 | }] 210 | }, { 211 | label: '&View', 212 | submenu: (process.env.NODE_ENV === 'development') ? [{ 213 | label: '&Reload', 214 | accelerator: 'Ctrl+R', 215 | click() { 216 | mainWindow.webContents.reload() 217 | } 218 | }, { 219 | label: 'Toggle &Full Screen', 220 | accelerator: 'F11', 221 | click() { 222 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 223 | } 224 | }, { 225 | label: 'Toggle &Developer Tools', 226 | accelerator: 'Alt+Ctrl+I', 227 | click() { 228 | mainWindow.toggleDevTools() 229 | } 230 | }] : [{ 231 | label: 'Toggle &Full Screen', 232 | accelerator: 'F11', 233 | click() { 234 | mainWindow.setFullScreen(!mainWindow.isFullScreen()) 235 | } 236 | }] 237 | }, { 238 | label: 'Help', 239 | submenu: [{ 240 | label: 'Learn More', 241 | click() { 242 | shell.openExternal('https://www.firestation.io') 243 | } 244 | }, { 245 | label: 'Documentation', 246 | click() { 247 | shell.openExternal('https://docs.firestation.io') 248 | } 249 | }, { 250 | label: 'View Source Code', 251 | click() { 252 | shell.openExternal('https://github.com/JoeRoddy/firestation') 253 | } 254 | }] 255 | }]; 256 | menu = Menu.buildFromTemplate(template) 257 | mainWindow.setMenu(menu) 258 | } 259 | }) 260 | -------------------------------------------------------------------------------- /app/components/object_tree/ObjectNode.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import typeName from "type-name"; 4 | import ReactTooltip from "react-tooltip"; 5 | import PropTypes from "prop-types"; 6 | import StringHelper from "../../helpers/StringHelper"; 7 | import UpdateService from "../../service/UpdateService"; 8 | import FirebaseService from "../../service/FirebaseService"; 9 | 10 | export default class ObjectNode extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.handleEditChange = this.handleEditChange.bind(this); 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | this.VALUE_EDIT = "FIRESTATION_RESERVED_VALUE_EDIT"; 16 | this.state = { 17 | opened: props.level > 0, 18 | keyEdit: false 19 | }; 20 | } 21 | 22 | componentWillReceiveProps(newProps) { 23 | if (this.props.value !== newProps.value) { 24 | this.setState({ opened: newProps.level > 0 }); 25 | } 26 | } 27 | 28 | toggleNode(e) { 29 | this.setState({ opened: !this.state.opened }); 30 | } 31 | 32 | render() { 33 | const { value } = this.props; 34 | const type = typeName(value); 35 | return /^(Array|Object)$/.test(type) 36 | ? this.renderObject(value, type) 37 | : /^(number|string|boolean|null)$/.test(type) 38 | ? this.renderValue(value, type) 39 | : this.renderOther(value, type); 40 | } 41 | 42 | deleteConfirmation(e, path) { 43 | e.stopPropagation(); 44 | const confirmationMsg = 45 | "warning All data at this location, including nested data, will be permanently deleted: \nData location: " + 46 | path; 47 | if (confirm(confirmationMsg)) { 48 | let db = FirebaseService.startFirebaseApp( 49 | this.props.store.currentDatabase 50 | ).database(); 51 | UpdateService.deleteObject(db, path); 52 | } 53 | } 54 | 55 | addProperty(path, value) {} 56 | 57 | sortByOrderBys(resultsArr) { 58 | let orderBys = this.props.store.results.orderBys; 59 | if (!orderBys) { 60 | return resultsArr; 61 | } 62 | 63 | const compare = (a, b) => { 64 | if (!a) { 65 | return -1; 66 | } else if (!b) { 67 | return 1; 68 | } 69 | if (typeof a === "string" || typeof b === "string") { 70 | return (a + "").toLowerCase().localeCompare((b + "").toLowerCase()); 71 | } else { 72 | return a - b; 73 | } 74 | }; 75 | 76 | //earliest orderBy's takes precedence, so we'll .reverse() 77 | orderBys.reverse().forEach(orderBy => { 78 | let propToSort = orderBy.propToSort; 79 | resultsArr.sort((a, b) => { 80 | a = a.value[propToSort]; 81 | b = b.value[propToSort]; 82 | if (!orderBy.ascending) { 83 | return compare(b, a); 84 | } else { 85 | return compare(a, b); 86 | } 87 | }); 88 | }); 89 | 90 | return resultsArr; 91 | } 92 | 93 | renderObject(obj, type) { 94 | const { path, level, store, fbPath } = this.props; 95 | const { opened } = this.state; 96 | const clevel = level > 0 ? level - 1 : 0; 97 | const that = this; 98 | let iter = 99 | type === "Array" 100 | ? obj.map((v, i) => ({ prop: i, value: v })) 101 | : Object.keys(obj) 102 | .sort(function(a, b) { 103 | return a.toLowerCase().localeCompare(b.toLowerCase()); 104 | }) 105 | .map(prop => ({ prop, value: obj[prop] })); 106 | if (level === 2) { 107 | iter = this.sortByOrderBys(iter); 108 | } 109 | return ( 110 |
    111 |
    112 | {clevel !== 1 && 113 | } 119 | 125 | {opened ? Collapse : Expand} Data 126 | 127 | {clevel !== 1 && 128 | that.props.creationPath === fbPath && 129 |
    130 |
    131 | this.setState({ newKey: e.target.value })} 135 | />{" "} 136 |
    137 | this.setState({ newVal: e.target.value })} 141 | /> 142 |
    143 |
    144 |
    145 | 151 | 157 |
    158 |
    } 159 | {clevel !== 1 && 160 | !that.props.creationPath && 161 | 162 | that.props.setCreationPath(fbPath)} 164 | data-tip 165 | data-for={"add-child " + fbPath} 166 | className="fa fa-plus" 167 | /> 168 | 174 | Add Property 175 | 176 | this.deleteConfirmation(e, fbPath)} 178 | data-tip 179 | data-for={"delete-child " + fbPath} 180 | className="fa fa-times" 181 | /> 182 | 188 | Delete Object 189 | 190 | } 191 |
    192 | 196 | 197 | {iter.map(({ prop, value }) => { 198 | const cpath = 199 | type === "Array" 200 | ? `${path}[${prop}]` 201 | : path ? `${path}.${prop}` : prop; 202 | const entireFbPath = 203 | fbPath + 204 | (fbPath.charAt(fbPath.length - 1) === "/" ? "" : "/") + 205 | prop; 206 | const handleClick = () => { 207 | this.setState({ keyEdit: true }); 208 | this.props.setPathUnderEdit(entireFbPath); 209 | }; 210 | 211 | return ( 212 | 213 | {this.props.pathUnderEdit && 214 | this.props.pathUnderEdit === entireFbPath 215 | ? 235 | : } 238 | {!this.props.noValue && 239 | } 271 | } 272 | 273 | ); 274 | })} 275 | 276 |
    { 218 | e.stopPropagation(); 219 | }} 220 | > 221 |
    222 | 229 |
    230 |
    this.props.setPathUnderEdit(null)} 233 | /> 234 |
    236 | {prop} 237 | 240 | 252 | {typeof value !== "object" && 253 | 254 | 258 | this.deleteConfirmation(e, entireFbPath)} 259 | className="fa fa-times delete-prop" 260 | aria-hidden="true" 261 | /> 262 | 268 | Delete Property 269 | 270 |
    277 |
    278 | ); 279 | } 280 | 281 | handleSubmit(e) { 282 | e.preventDefault(); 283 | let db = FirebaseService.startFirebaseApp( 284 | this.props.store.currentDatabase 285 | ).database(); 286 | let newValue = StringHelper.getParsedValue(this.state.newVal); 287 | let path = this.props.fbPath; 288 | const pathUnderEdit = this.props.pathUnderEdit; 289 | let keyChangeConfirmed = false; 290 | const keyConfirmationMsg = 291 | "This will permanently move all child data.\n Data location: " + 292 | pathUnderEdit + " ---> "+path+newValue; 293 | if (pathUnderEdit && this.state.keyEdit && confirm(keyConfirmationMsg)) { 294 | keyChangeConfirmed = true; 295 | let newObject = this.props.value; 296 | let oldKey = pathUnderEdit.substring(pathUnderEdit.lastIndexOf("/") + 1); 297 | newObject[newValue] = newObject[oldKey]; 298 | delete newObject[oldKey]; 299 | newValue = newObject; 300 | } 301 | 302 | 303 | if (!this.state.keyEdit || keyChangeConfirmed) { 304 | UpdateService.set(db, path, newValue); 305 | } 306 | 307 | this.setState({ newVal: null, keyEdit: false }); 308 | this.props.setPathUnderEdit(null); 309 | } 310 | 311 | handleEditChange(e) { 312 | this.setState({ newVal: e.target.value }); 313 | } 314 | 315 | createNewProperty(e) { 316 | let db = FirebaseService.startFirebaseApp( 317 | this.props.store.currentDatabase 318 | ).database(); 319 | UpdateService.set( 320 | db, 321 | this.props.creationPath + "/" + this.state.newKey, 322 | this.state.newVal 323 | ); 324 | this.props.setCreationPath(null); 325 | } 326 | 327 | renderValue(value, type) { 328 | return ( 329 |
    330 | {this.props.pathUnderEdit === this.props.fbPath + this.props.prop 331 | ?
    332 |
    333 | e.stopPropagation()} 340 | /> 341 | this.props.setPathUnderEdit(null)} 344 | /> 345 | 346 |
    347 | :
    350 | this.props.setPathUnderEdit( 351 | this.props.fbPath + this.props.prop 352 | )} 353 | > 354 | 355 | {JSON.stringify(value)} 356 | 357 |
    } 358 |
    359 | ); 360 | } 361 | 362 | renderOther(value, type) { 363 | return ( 364 |
    365 |
    366 | {/*{'(' + type + ')'}*/} 367 |
    368 |
    369 | ); 370 | } 371 | } 372 | 373 | ObjectNode.propTypes = { 374 | value: PropTypes.any.isRequired, 375 | path: PropTypes.string.isRequired, 376 | level: PropTypes.number.isRequired 377 | }; 378 | -------------------------------------------------------------------------------- /app/assets/stylesheets/base.scss: -------------------------------------------------------------------------------- 1 | $gray: #aaa; 2 | $break-small: 800px; 3 | $break-large: 1300px; 4 | $break-short: 960px; 5 | 6 | @mixin respond-to($media) { 7 | @if $media==small { 8 | @media only screen and (max-width: $break-small) { 9 | @content; 10 | } 11 | } @else if $media==medium-screens { 12 | @media only screen and (min-width: $break-small + 1) and (max-width: $break-large - 1) { 13 | @content; 14 | } 15 | } @else if $media==wide-screens { 16 | @media only screen and (min-width: $break-large) { 17 | @content; 18 | } 19 | } @else if $media==short { 20 | @media only screen and (max-height: $break-short) { 21 | @content; 22 | } 23 | } 24 | } 25 | 26 | @mixin non-highlightable() { 27 | } 28 | 29 | $lavender: #9370db; 30 | $turq: #0fb8ad; 31 | $turqhover: #1fc8db; 32 | #root { 33 | //using # to override bootstrap 34 | font-family: "Roboto", 'Helvetica Neue, Helvetica, Arial', sans-serif; 35 | } 36 | 37 | body { 38 | font-style: normal; 39 | font-weight: 300; 40 | font-size: 14px; 41 | line-height: 1.4; 42 | color: #212121; 43 | overflow-x: hidden; 44 | -webkit-font-smoothing: antialiased; 45 | text-rendering: optimizeLegibility; 46 | } 47 | 48 | .App { 49 | // min-height: 80vh; 50 | .App-Body { 51 | } 52 | } 53 | 54 | .Workstation { 55 | display: flex; 56 | height: 100%; 57 | margin-left: 260px; 58 | overflow: hidden !important; 59 | @include respond-to(small) { 60 | margin-left: 0px; 61 | } 62 | .workstation-dbTitle { 63 | margin-top: 7vh; 64 | @include respond-to(short) { 65 | visibility: hidden; 66 | margin-top: 2vh; 67 | } 68 | } 69 | .workArea { 70 | width: 100%; 71 | display: block; 72 | } 73 | .commitbtn { 74 | margin-right: 5px; 75 | } 76 | .workstation-btns { 77 | display: flex; 78 | justify-content: space-between; 79 | } 80 | .util-btns { 81 | display: flex; 82 | margin-right: 15px; 83 | button { 84 | margin-left: 3px; 85 | } 86 | } 87 | .workstation-underWorkbook { 88 | height: 53vh; 89 | width: 100%; 90 | display: flex; 91 | flex-direction: column; 92 | overflow: hidden; 93 | justify-content: flex-start; 94 | .objectTree-container { 95 | h4 { 96 | display: inline-block; 97 | } 98 | .results-header { 99 | .gray-icon { 100 | float: right; 101 | background-color: white; 102 | color: $gray; 103 | font-size: 30px; 104 | cursor: pointer; 105 | } 106 | } 107 | display: flex; 108 | flex-direction: column; 109 | flex: 1.5; 110 | overflow-x: hidden; 111 | overflow-y: hidden; 112 | position: relative; 113 | } 114 | 115 | .QueryHistory { 116 | display: flex; 117 | flex-direction: column; 118 | margin-top: 1vh; 119 | flex: 1; 120 | .histTable-container { 121 | overflow: scroll; 122 | overflow-x: hidden; 123 | } 124 | .clickable { 125 | cursor: pointer; 126 | &:hover { 127 | background-color: lightgray; 128 | } 129 | } 130 | } 131 | .ObjectTree { 132 | display: flex; 133 | overflow-y: scroll; 134 | overflow-x: hidden; 135 | } 136 | .queryError { 137 | color: red; 138 | } 139 | } 140 | .resultsCollapsed { 141 | .objectTree-container { 142 | flex: .1 !important; 143 | } 144 | } 145 | } 146 | 147 | .Workbook { 148 | .AceEditor { 149 | textarea { 150 | // width: 100% !important; 151 | // height: 20vh !important; 152 | } 153 | margin-bottom: 7px; 154 | } 155 | .commitbtn { 156 | margin-right: 5px; 157 | } 158 | } 159 | 160 | .Modal { 161 | position: fixed; 162 | z-index: 6; 163 | left: 0; 164 | top: 0; 165 | width: 100%; 166 | height: 100%; 167 | overflow: none; 168 | background-color: rgb(0, 0, 0); 169 | background-color: rgba(0, 0, 0, 0.4); 170 | .modal-content { 171 | position: relative; 172 | margin-left: 6vw; 173 | background-color: #fefefe; 174 | margin: 15% auto; 175 | padding: 20px; 176 | border: 1px solid #888; 177 | width: 550px; 178 | } 179 | .closeBtn { 180 | position: absolute; 181 | top: 9px; 182 | right: 9px; 183 | } 184 | } 185 | 186 | .detailText { 187 | color: #7b879e; 188 | font-size: 14px; 189 | font-family: Lato, sans-serif; 190 | &.sm { 191 | font-size: 12px; 192 | } 193 | } 194 | 195 | .AddDatabase { 196 | h5 { 197 | display: flex; 198 | span { 199 | margin-top: 8px; 200 | margin-left: 8px; 201 | } 202 | small { 203 | margin-left: 4px; 204 | font-size: 12px; 205 | font-style: italic; 206 | } 207 | } 208 | a { 209 | color: #0000ee !important; 210 | text-decoration: underline !important; 211 | cursor: pointer; 212 | } 213 | .bt, 214 | .white { 215 | margin-right: 15px; 216 | } 217 | } 218 | 219 | .DatabaseConfig { 220 | .serviceAcctEdit { 221 | display: flex; 222 | button { 223 | margin-right: 10px; 224 | } 225 | margin-bottom: 20px; 226 | } 227 | .nameEdit { 228 | display: flex; 229 | input { 230 | margin-left: 10px; 231 | margin-bottom: 8px; 232 | } 233 | margin-bottom: 16px; 234 | } 235 | .red { 236 | margin-left: 10px; 237 | } 238 | } 239 | 240 | b { 241 | font-weight: bold; 242 | } 243 | 244 | i { 245 | font-style: italic; 246 | } 247 | 248 | .Sidemenu { 249 | height: 100%; 250 | width: 250px; 251 | padding-top: 80px; 252 | position: fixed; 253 | z-index: 1; 254 | top: 0; 255 | left: 0; 256 | background: linear-gradient(141deg, $turq 0%, $turqhover 51%, #2cb5e8 75%); 257 | display: flex; 258 | flex-direction: column; 259 | @include respond-to(small) { 260 | width: 0px; 261 | display: none; 262 | } 263 | a { 264 | @extend .noselect; 265 | margin-top: 2.5px; 266 | margin-bottom: 2.5px; 267 | padding-left: 30px; 268 | width: 100%; 269 | height: 30px; 270 | color: white !important; 271 | font-size: 20px; 272 | cursor: pointer; 273 | font-weight: 500; 274 | padding-top: 2px; 275 | &:hover { 276 | background: #267e88 !important; 277 | } 278 | } 279 | .sidemenu-savedQueries { 280 | margin-top: .5vh; 281 | display: flex; 282 | flex-direction: column; 283 | align-items: center; 284 | } 285 | .sidemenu-savedQuery { 286 | width: 130px; 287 | display: flex; 288 | justify-content: space-between; 289 | color: #e5e2dc; 290 | cursor: pointer; 291 | i { 292 | display: none; 293 | } 294 | i:hover { 295 | color: red; 296 | } 297 | &:hover { 298 | color: white !important; 299 | i { 300 | display: block; 301 | } 302 | } 303 | } 304 | .firebase-sidemenu-icon { 305 | width: 16px; 306 | margin-right: 5px; 307 | margin-left: 2px; 308 | padding-bottom: 5px; 309 | } 310 | } 311 | 312 | .Landing { 313 | margin-top: 100px; 314 | } 315 | 316 | .Demo { 317 | } 318 | 319 | h1, 320 | h2 { 321 | color: $turq !important; 322 | } 323 | 324 | h3 { 325 | color: goldenrod; 326 | } 327 | 328 | h4 { 329 | color: $turq; 330 | font-weight: bold; 331 | } 332 | 333 | .navbar { 334 | z-index: 5; 335 | display: flex !important; 336 | flex-direction: row; 337 | text-align: center; 338 | -webkit-box-shadow: 0 8px 6px -6px #999; 339 | -moz-box-shadow: 0 8px 6px -6px #999; 340 | box-shadow: 0 8px 6px -6px #999; 341 | @extend .noselect; 342 | span { 343 | color: white; 344 | } 345 | 346 | background: linear-gradient(141deg, #f4c242 0%, #e2c743 51%, #e2a844 75%); 347 | li { 348 | color: white !important; 349 | } 350 | .nav-db { 351 | a { 352 | color: white !important; 353 | } 354 | padding-top: .2vh; 355 | } 356 | .navbar-brand { 357 | display: flex; 358 | img { 359 | height:25px; 360 | } 361 | span { 362 | padding-top: .2vh; 363 | } 364 | } 365 | .nav-item.dropdown { 366 | padding-top: .4vh; 367 | } 368 | .nav-link.dropdown-toggle { 369 | color: white !important; 370 | cursor: pointer; 371 | } 372 | .dropdown-menu { 373 | .dropdown-toggle { 374 | background-color: transparent !important; 375 | } 376 | a { 377 | cursor: pointer; 378 | } 379 | a:hover { 380 | color: $turqhover; 381 | } 382 | a { 383 | color: $turq; 384 | } 385 | } 386 | .navbar-collapse.collapse { 387 | .dropdown-menu { 388 | position: absolute !important; 389 | } 390 | } 391 | } 392 | 393 | //// BUTTON CSS //// 394 | i.fa-times { 395 | cursor: pointer; 396 | &:hover { 397 | color: red; 398 | } 399 | } 400 | 401 | .closeBtn { 402 | color: #aaa; 403 | float: right; 404 | font-size: 16px; 405 | font-weight: bold; 406 | } 407 | 408 | .bt { 409 | width: 120px; 410 | height: 40px; 411 | padding: 0; 412 | overflow: hidden; 413 | border-width: 0; 414 | outline: none; 415 | border-radius: 2px; 416 | box-shadow: 0 1px 4px rgba(0, 0, 0, .6); 417 | background-color: #2ecc71; 418 | color: #ecf0f1; 419 | transition: background-color .3s; 420 | cursor: pointer; 421 | } 422 | 423 | .bt:hover, 424 | .bt:focus { 425 | background-color: #27ae60; 426 | } 427 | 428 | .bt > * { 429 | position: relative; 430 | } 431 | 432 | .bt span { 433 | display: block; 434 | padding: 12px 24px; 435 | } 436 | 437 | .bt:before { 438 | content: ""; 439 | display: block; 440 | width: 0; 441 | padding-top: 0; 442 | border-radius: 70%; 443 | background-color: rgba(236, 240, 241, .3); 444 | -webkit-transform: translate(-50%, -50%); 445 | -moz-transform: translate(-50%, -50%); 446 | -ms-transform: translate(-50%, -50%); 447 | -o-transform: translate(-50%, -50%); 448 | transform: translate(-50%, -50%); 449 | } 450 | 451 | .bt:active:before { 452 | width: 120%; 453 | padding-top: 120%; 454 | transition: width .2s ease-out, padding-top .2s ease-out; 455 | } 456 | 457 | *, 458 | *:before, 459 | *:after { 460 | box-sizing: border-box; 461 | } 462 | 463 | .bt.blue { 464 | background-color: #0fb8ad; 465 | &:hover { 466 | background-color: #1fc8db; // color: white; 467 | } 468 | } 469 | 470 | .bt.red { 471 | background-color: #f4425f; 472 | &:hover { 473 | background-color: #ad2d41; // color: white; 474 | } 475 | } 476 | 477 | .bt.white { 478 | background-color: #9191a3; 479 | &:hover { 480 | background-color: #565666; // color: white; 481 | } 482 | } 483 | 484 | .bt.sm { 485 | width: 40px; 486 | } 487 | 488 | //// END - BUTTON CSS //// 489 | //// OBJECT TREE //// 490 | .onClickOutside { 491 | position: fixed; 492 | z-index: 0; 493 | left: 0; 494 | top: 0; 495 | width: 100%; 496 | height: 100%; 497 | } 498 | 499 | .object-tree { 500 | z-index: 2; 501 | .editable { 502 | &:hover { 503 | outline: 2px solid black; 504 | } 505 | } 506 | .object-node { 507 | .object-label { 508 | cursor: pointer; 509 | -webkit-user-select: none; 510 | -moz-user-select: none; 511 | -ms-user-select: none; 512 | user-select: none; 513 | .toggle-icon { 514 | display: inline-block; 515 | width: 12px; 516 | height: 12px; 517 | font-size: 14px; 518 | line-height: 12px; 519 | text-align: center; 520 | font-style: normal; 521 | border: 1px dotted #aaa; 522 | &:before { 523 | content: "+"; 524 | } 525 | &.opened { 526 | &:before { 527 | content: "-"; 528 | } 529 | } 530 | } 531 | .object-type { 532 | font-style: italic; 533 | } 534 | i { 535 | display: none; 536 | margin-top: 1px; 537 | margin-left: 3px; 538 | } 539 | &:hover { 540 | i { 541 | display: inline-block; 542 | } 543 | i.fa-times { 544 | color: red; 545 | &:hover { 546 | color: black; 547 | } 548 | } 549 | i.fa-plus { 550 | color: green; 551 | } 552 | } 553 | .new-prop { 554 | display: flex; 555 | margin-bottom: 5px; 556 | .new-prop-btns { 557 | display: flex; 558 | flex-direction: column; 559 | justify-content: space-around; 560 | } 561 | button { 562 | height: 18px; 563 | width: 55px; 564 | } 565 | } 566 | } 567 | table { 568 | i { 569 | display: none; 570 | } 571 | width: 100%; 572 | tr { 573 | vertical-align: top; 574 | th.prop-name { 575 | cursor: text; 576 | padding: 4px 8px; 577 | text-align: left; 578 | background-color: #eee; 579 | color: #333; 580 | input { 581 | border: 0; 582 | outline: 0px; 583 | } 584 | &:hover { 585 | // border: 2px solid black; 586 | background-color: white; 587 | -webkit-box-shadow: inset 0px 0px 0px 2px black; 588 | -moz-box-shadow: inset 0px 0px 0px 2px black; 589 | box-shadow: inset 0px 0px 0px 2px black; 590 | } 591 | } 592 | td.prop-value { 593 | cursor: text; 594 | padding: 4px 8px; 595 | width: 145%; 596 | border-top: 1px solid #aaa; 597 | display: flex; 598 | min-width: 15vw; 599 | div { 600 | min-width: 8vw; 601 | } 602 | } 603 | tr:hover > .prop-value > th > .delete-prop { 604 | display: block; 605 | cursor: pointer; 606 | color: red; 607 | margin-left: 5px; 608 | 609 | &:hover { 610 | color: black; 611 | } 612 | } 613 | } 614 | } 615 | } 616 | } 617 | 618 | //// END - OBJECT TREE //// 619 | //// SLIDER TOGGLE CSS //// 620 | 621 | /* The switch - the box around the slider */ 622 | 623 | .switch { 624 | position: relative; 625 | display: inline-block; 626 | width: 60px; 627 | height: 34px; 628 | } 629 | 630 | /* Hide default HTML checkbox */ 631 | 632 | .switch input { 633 | display: none; 634 | } 635 | 636 | /* The slider */ 637 | 638 | .slider { 639 | position: absolute; 640 | cursor: pointer; 641 | top: 0; 642 | left: 0; 643 | right: 0; 644 | bottom: 0; 645 | background-color: #ccc; 646 | -webkit-transition: .4s; 647 | transition: .4s; 648 | } 649 | 650 | .slider:before { 651 | position: absolute; 652 | content: ""; 653 | height: 26px; 654 | width: 26px; 655 | left: 4px; 656 | bottom: 4px; 657 | background-color: white; 658 | -webkit-transition: .4s; 659 | transition: .4s; 660 | } 661 | 662 | input:checked + .slider { 663 | background-color: $turq; 664 | } 665 | 666 | input:focus + .slider { 667 | box-shadow: 0 0 1px $turqhover; 668 | } 669 | 670 | input:checked + .slider:before { 671 | -webkit-transform: translateX(26px); 672 | -ms-transform: translateX(26px); 673 | transform: translateX(26px); 674 | } 675 | 676 | /* Rounded sliders */ 677 | 678 | .slider.round { 679 | border-radius: 34px; 680 | } 681 | 682 | .slider.round:before { 683 | border-radius: 50%; 684 | } 685 | .noselect { 686 | -webkit-touch-callout: none; /* iOS Safari */ 687 | -webkit-user-select: none; /* Safari */ 688 | -khtml-user-select: none; /* Konqueror HTML */ 689 | -moz-user-select: none; /* Firefox */ 690 | -ms-user-select: none; /* Internet Explorer/Edge */ 691 | user-select: none; /* Non-prefixed version, currently 692 | supported by Chrome and Opera */ 693 | } 694 | 695 | //// END: SLIDER TOGGLE //// 696 | -------------------------------------------------------------------------------- /app/helpers/QueryHelper.js: -------------------------------------------------------------------------------- 1 | import StringHelper from "./StringHelper"; 2 | import UpdateService from "../service/UpdateService"; 3 | import FirebaseService from "../service/FirebaseService"; 4 | import { isValidDate, executeDateComparison } from "../helpers/DateHelper"; 5 | const NO_EQUALITY_STATEMENTS = "NO_EQUALITY_STATEMENTS"; 6 | const SELECT_STATEMENT = "SELECT_STATEMENT"; 7 | const UPDATE_STATEMENT = "UPDATE_STATEMENT"; 8 | const INSERT_STATEMENT = "INSERT_STATEMENT"; 9 | const DELETE_STATEMENT = "DELETE_STATEMENT"; 10 | const FIRESTATION_DATA_PROP = "FIRESTATION_DATA_PROP"; 11 | const EQUATION_IDENTIFIERS = [" / ", " + ", " - ", " * "]; 12 | 13 | export default class QueryHelper { 14 | static getRootKeysPromise(database) { 15 | if (!database) { 16 | return null; 17 | } 18 | const url = 19 | "https://" + 20 | database.config.projectId + 21 | ".firebaseio.com//.json?shallow=true"; 22 | return fetch(url).then(response => { 23 | return response.json(); 24 | }); 25 | } 26 | 27 | static executeQuery(query, database, callback, commitResults) { 28 | let app = FirebaseService.startFirebaseApp(database); 29 | let db = app.database(); 30 | let ref = db.ref("/"); 31 | ref.off("value"); 32 | const statementType = this.determineQueryType(query); 33 | if (statementType === SELECT_STATEMENT) { 34 | this.executeSelect(query, db, callback); 35 | } else if (statementType === UPDATE_STATEMENT) { 36 | return this.executeUpdate(query, db, callback, commitResults); 37 | } else if (statementType === DELETE_STATEMENT) { 38 | return this.executeDelete(query, db, callback, commitResults); 39 | } else if (statementType === INSERT_STATEMENT) { 40 | return this.executeInsert(query, db, callback, commitResults); 41 | } 42 | } 43 | 44 | static formatAndCleanQuery(query) { 45 | //called by App.jsx to remove comments before saving to history 46 | query = StringHelper.replaceAll(query, /(\/\/|--).+/, ""); 47 | query = query.replace(/\r?\n|\r/g, " "); 48 | return query; 49 | } 50 | 51 | static executeInsert(query, db, callback, commitResults) { 52 | const collection = this.getCollection(query, INSERT_STATEMENT); 53 | const that = this; 54 | const insertObjects = this.getObjectsFromInsert(query); 55 | const insertCount = this.getInsertCount(query); 56 | const path = collection + "/"; 57 | if (commitResults) { 58 | for (let i = 1; i < insertCount; i++) { 59 | //insert clones 60 | UpdateService.pushObject(db, path, insertObjects[0]); 61 | } 62 | for (let key in insertObjects) { 63 | UpdateService.pushObject(db, path, insertObjects[key]); 64 | } 65 | } 66 | let results = { 67 | insertCount: insertCount, 68 | statementType: INSERT_STATEMENT, 69 | payload: insertObjects, 70 | path: path 71 | }; 72 | callback(results); 73 | } 74 | 75 | static executeDelete(query, db, callback, commitResults) { 76 | const collection = this.getCollection(query, DELETE_STATEMENT); 77 | const that = this; 78 | this.getWheres(query, db, wheres => { 79 | this.getDataForSelect(db, collection, null, wheres, null, dataToAlter => { 80 | if (dataToAlter && commitResults) { 81 | Object.keys(dataToAlter.payload).forEach(function(objKey, index) { 82 | const path = collection + "/" + objKey; 83 | UpdateService.deleteObject(db, path); 84 | }); 85 | } 86 | let results = { 87 | statementType: DELETE_STATEMENT, 88 | payload: dataToAlter.payload, 89 | firebaseListener: dataToAlter.firebaseListener, 90 | path: collection 91 | }; 92 | callback(results); 93 | }); 94 | }); 95 | } 96 | 97 | static executeSelect(query, db, callback) { 98 | const collection = this.getCollection(query, SELECT_STATEMENT); 99 | const orderBys = this.getOrderBys(query); 100 | const selectedFields = this.getSelectedFields(query); 101 | this.getWheres(query, db, wheres => { 102 | this.getDataForSelect( 103 | db, 104 | collection, 105 | selectedFields, 106 | wheres, 107 | orderBys, 108 | callback 109 | ); 110 | }); 111 | } 112 | 113 | static executeUpdate(query, db, callback, commitResults) { 114 | const collection = this.getCollection(query, UPDATE_STATEMENT); 115 | const sets = this.getSets(query); 116 | if (!sets) { 117 | return null; 118 | } 119 | const that = this; 120 | this.getWheres(query, db, wheres => { 121 | this.getDataForSelect(db, collection, null, wheres, null, dataToAlter => { 122 | let data = dataToAlter.payload; 123 | Object.keys(data).forEach(function(objKey, index) { 124 | that.updateItemWithSets(data[objKey], sets); 125 | const path = collection + "/" + objKey; 126 | if (commitResults) { 127 | UpdateService.updateFields( 128 | db, 129 | path, 130 | data[objKey], 131 | Object.keys(sets) 132 | ); 133 | } 134 | }); 135 | let results = { 136 | statementType: UPDATE_STATEMENT, 137 | payload: data, 138 | firebaseListener: dataToAlter.firebaseListener, 139 | path: collection 140 | }; 141 | callback(results); 142 | }); 143 | }); 144 | } 145 | 146 | static getDataForSelect( 147 | db, 148 | collection, 149 | selectedFields, 150 | wheres, 151 | orderBys, 152 | callback 153 | ) { 154 | console.log( 155 | "getData (collection, selectedFields, wheres):", 156 | collection, 157 | selectedFields, 158 | wheres 159 | ); 160 | var ref = db.ref(collection); 161 | let results = { 162 | queryType: SELECT_STATEMENT, 163 | path: collection, 164 | orderBys: orderBys, 165 | firebaseListener: ref 166 | }; 167 | if (!selectedFields && !wheres) { 168 | ref = db.ref(collection); 169 | ref.on("value", snapshot => { 170 | results.payload = snapshot.val(); 171 | return callback(results); 172 | }); 173 | } else if (!wheres) { 174 | ref.on("value", snapshot => { 175 | results.payload = snapshot.val(); 176 | if (selectedFields) { 177 | results.payload = this.removeNonSelectedFieldsFromResults( 178 | results.payload, 179 | selectedFields 180 | ); 181 | } 182 | return callback(results); 183 | }); 184 | } else { 185 | let mainWhere = wheres[0]; 186 | if (mainWhere.error && mainWhere.error === NO_EQUALITY_STATEMENTS) { 187 | ref.on("value", snapshot => { 188 | results.payload = this.filterWheresAndNonSelectedFields( 189 | snapshot.val(), 190 | wheres, 191 | selectedFields 192 | ); 193 | return callback(results); 194 | }); 195 | } else { 196 | ref 197 | .orderByChild(mainWhere.field) 198 | .equalTo(mainWhere.value) 199 | .on("value", snapshot => { 200 | results.payload = this.filterWheresAndNonSelectedFields( 201 | snapshot.val(), 202 | wheres, 203 | selectedFields 204 | ); 205 | console.log("select results: ", results); 206 | 207 | return callback(results); 208 | }); 209 | } 210 | } 211 | } 212 | 213 | static updateItemWithSets(obj, sets) { 214 | const that = this; 215 | Object.keys(sets).forEach(function(objKey, index) { 216 | const thisSet = sets[objKey]; 217 | if ( 218 | thisSet && 219 | typeof thisSet === "object" && 220 | thisSet.hasOwnProperty(FIRESTATION_DATA_PROP) 221 | ) { 222 | const newVal = thisSet.FIRESTATION_DATA_PROP; 223 | for (let i = 0; i < EQUATION_IDENTIFIERS.length; i++) { 224 | if (newVal.includes(EQUATION_IDENTIFIERS[i])) { 225 | obj[objKey] = that.executeUpdateEquation( 226 | obj, 227 | thisSet.FIRESTATION_DATA_PROP 228 | ); 229 | return; 230 | } 231 | } 232 | //not an equation, treat it as an individual prop 233 | let finalValue = obj[newVal]; 234 | if (newVal.includes(".")) { 235 | let props = newVal.split("."); 236 | finalValue = obj[props[0]]; 237 | for (let i = 1; i < props.length; i++) { 238 | finalValue = finalValue[props[i]]; 239 | } 240 | } 241 | obj[objKey] = finalValue; 242 | } else { 243 | obj[objKey] = thisSet; 244 | } 245 | }); 246 | return obj; 247 | } 248 | 249 | static executeUpdateEquation(existingObject, equation) { 250 | //replace variable names with corresponding values: 251 | existingObject && 252 | Object.keys(existingObject).forEach(key => { 253 | let newValue = existingObject[key]; 254 | if (typeof newValue !== "number") { 255 | newValue = '"' + newValue + '"'; 256 | } 257 | equation = StringHelper.replaceAll(equation, key, newValue); 258 | }); 259 | //execute 260 | return eval(equation); 261 | } 262 | 263 | static determineQueryType(query) { 264 | let q = query.trim(); 265 | let firstTerm = q.split(" ")[0].trim().toLowerCase(); 266 | switch (firstTerm) { 267 | case "select": 268 | return SELECT_STATEMENT; 269 | case "update": 270 | return UPDATE_STATEMENT; 271 | case "insert": 272 | return INSERT_STATEMENT; 273 | case "delete": 274 | return DELETE_STATEMENT; 275 | default: 276 | return SELECT_STATEMENT; 277 | } 278 | } 279 | 280 | static getWheres(query, db, callback) { 281 | const whereIndexStart = query.indexOf(" where ") + 1; 282 | if (whereIndexStart < 1) { 283 | return callback(null); 284 | } 285 | const orderByIndex = query.toUpperCase().indexOf("ORDER BY"); 286 | const whereIndexEnd = orderByIndex >= 0 ? orderByIndex : query.length; 287 | let wheresArr = query 288 | .substring(whereIndexStart + 5, whereIndexEnd) 289 | .split(" and "); 290 | wheresArr[wheresArr.length - 1] = wheresArr[wheresArr.length - 1].replace( 291 | ";", 292 | "" 293 | ); 294 | let wheres = []; 295 | wheresArr.forEach(where => { 296 | where = StringHelper.replaceAllIgnoreCase(where, "not like", "!like"); 297 | let eqCompAndIndex = this.determineComparatorAndIndex(where); 298 | let whereObj = { 299 | field: StringHelper.replaceAll( 300 | where.substring(0, eqCompAndIndex.index).trim(), 301 | "\\.", 302 | "/" 303 | ), 304 | comparator: eqCompAndIndex.comparator 305 | }; 306 | let val = StringHelper.getParsedValue( 307 | where 308 | .substring(eqCompAndIndex.index + eqCompAndIndex.comparator.length) 309 | .trim() 310 | ); 311 | if ( 312 | typeof val === "string" && 313 | val.charAt(0) === "(" && 314 | val.charAt(val.length - 1) === ")" 315 | ) { 316 | this.executeSelect(val.substring(1, val.length - 1), db, results => { 317 | whereObj.value = results.payload; 318 | wheres.push(whereObj); 319 | if (wheresArr.length === wheres.length) { 320 | return callback(this.optimizeWheres(wheres)); 321 | } 322 | }); 323 | } else { 324 | whereObj.value = val; 325 | wheres.push(whereObj); 326 | if (wheresArr.length === wheres.length) { 327 | return callback(this.optimizeWheres(wheres)); 328 | } 329 | } 330 | }); 331 | } 332 | 333 | static getSets(query) { 334 | const setIndexStart = query.indexOf(" set ") + 1; 335 | if (setIndexStart < 1) { 336 | return null; 337 | } 338 | const whereIndexStart = query.indexOf(" where ") + 1; 339 | let setsArr; 340 | if (whereIndexStart > 0) { 341 | setsArr = query.substring(setIndexStart + 3, whereIndexStart).split(", "); 342 | } else { 343 | setsArr = query.substring(setIndexStart + 3).split(", "); 344 | setsArr[setsArr.length - 1] = setsArr[setsArr.length - 1].replace( 345 | ";", 346 | "" 347 | ); 348 | } 349 | let sets = {}; 350 | setsArr.forEach(item => { 351 | let keyValSplit = item.split("="); 352 | if (keyValSplit.length === 2) { 353 | let key = keyValSplit[0].replace(".", "/").trim(); 354 | sets[key] = StringHelper.getParsedValue(keyValSplit[1].trim(), true); 355 | } 356 | }); 357 | return sets; 358 | } 359 | 360 | static getOrderBys(query) { 361 | let caps = query.toUpperCase(); 362 | const ORDER_BY = "ORDER BY"; 363 | let index = caps.indexOf(ORDER_BY); 364 | if (index < 0) { 365 | return null; 366 | } 367 | let orderByStr = query.substring(index + ORDER_BY.length); 368 | let split = orderByStr.split(","); 369 | let orderBys = split.map(orderBy => { 370 | let propToSort = orderBy.replace(";", "").trim(); 371 | propToSort = 372 | propToSort.indexOf(" ") >= 0 373 | ? propToSort.substring(0, propToSort.indexOf(" ")) 374 | : propToSort; 375 | let orderByObj = { 376 | ascending: true, 377 | propToSort: propToSort.trim() 378 | }; 379 | if (orderBy.toUpperCase().includes("DESC")) { 380 | orderByObj.ascending = false; 381 | } 382 | return orderByObj; 383 | }); 384 | return orderBys; 385 | } 386 | 387 | static filterWheresAndNonSelectedFields(results, wheres, selectedFields) { 388 | if (wheres.length > 1) { 389 | results = this.filterResultsByWhereStatements(results, wheres.slice(1)); 390 | } 391 | if (selectedFields) { 392 | results = this.removeNonSelectedFieldsFromResults( 393 | results, 394 | selectedFields 395 | ); 396 | } 397 | return results; 398 | } 399 | 400 | static getCollection(q, statementType) { 401 | let query = q.replace(/\(.*\)/, "").trim(); //removes nested selects 402 | let terms = query.split(" "); 403 | if (statementType === UPDATE_STATEMENT) { 404 | return StringHelper.replaceAll(terms[1], /\./, "/"); 405 | } else if (statementType === SELECT_STATEMENT) { 406 | if (terms.length === 2 && terms[0] === "from") { 407 | return StringHelper.replaceAll(terms[1], ".", "/"); 408 | } else if (terms.length === 1) { 409 | let collection = terms[0].replace(";", ""); 410 | return StringHelper.replaceAll(collection, /\./, "/"); 411 | } 412 | let collectionIndexStart = query.indexOf("from ") + 4; 413 | if (collectionIndexStart < 0) { 414 | throw "Error determining collection."; 415 | } 416 | if (collectionIndexStart < 5) { 417 | return StringHelper.replaceAll(terms[0], /\./, "/"); 418 | } 419 | let trimmedCol = query.substring(collectionIndexStart).trim(); 420 | let collectionIndexEnd = trimmedCol.match(/\ |;|$/).index; 421 | let collection = trimmedCol.substring(0, collectionIndexEnd); 422 | return StringHelper.replaceAll(collection, /\./, "/"); 423 | } else if (statementType === INSERT_STATEMENT) { 424 | let collectionToInsert = 425 | terms[1].toUpperCase() === "INTO" ? terms[2] : terms[3]; 426 | return StringHelper.replaceAll(collectionToInsert, /\./, "/"); 427 | } else if (statementType === DELETE_STATEMENT) { 428 | let index = terms.length > 2 ? 2 : 1; 429 | let term = StringHelper.replaceAll(terms[index], /;/, ""); 430 | return StringHelper.replaceAll(term, /\./, "/"); 431 | } 432 | throw "Error determining collection."; 433 | } 434 | 435 | static getSelectedFields(q) { 436 | let query = q.trim(); 437 | if (!query.startsWith("select ") || query.startsWith("select *")) { 438 | return null; 439 | } 440 | let regExp = /(.*select\s+)(.*)(\s+from.*)/; 441 | let froms = query.replace(regExp, "$2"); 442 | if (froms.length === query.length) { 443 | return null; 444 | } 445 | let fields = froms.split(","); 446 | if (fields.length === 0) { 447 | return null; 448 | } 449 | let selectedFields = {}; 450 | fields.map(field => { 451 | selectedFields[field.trim()] = true; 452 | }); 453 | return selectedFields; 454 | } 455 | 456 | static getObjectsFromInsert(query) { 457 | let valuesStr = query.match(/(values).+\);/)[0]; 458 | let keysStr = query.substring(query.indexOf("(") + 1, query.indexOf(")")); 459 | let keys = keysStr.split(","); 460 | let valuesStrArr = valuesStr.split("("); 461 | valuesStrArr.shift(); //removes "values (" 462 | let valuesArr = valuesStrArr.map(valueStr => { 463 | return valueStr.substring(0, valueStr.indexOf(")")).split(","); 464 | }); 465 | 466 | if (!keys || !valuesArr) { 467 | throw "Badly formatted insert statement"; 468 | } 469 | 470 | let insertObjects = {}; 471 | valuesArr.forEach((values, i) => { 472 | let insertObject = {}; 473 | keys.forEach((key, i) => { 474 | insertObject[ 475 | StringHelper.getParsedValue(key.trim()) 476 | ] = StringHelper.getParsedValue(values[i].trim()); 477 | }); 478 | insertObjects["pushId_" + i] = insertObject; 479 | }); 480 | 481 | return insertObjects; 482 | } 483 | 484 | static removeNonSelectedFieldsFromResults(results, selectedFields) { 485 | if (!results || !selectedFields) { 486 | return results; 487 | } 488 | Object.keys(results).forEach(function(objKey, index) { 489 | if (typeof results[objKey] !== "object") { 490 | if (!selectedFields[objKey]) { 491 | delete results[objKey]; 492 | } 493 | } else { 494 | Object.keys(results[objKey]).forEach(function(propKey, index) { 495 | if (!selectedFields[propKey]) { 496 | delete results[objKey][propKey]; 497 | } 498 | }); 499 | } 500 | }); 501 | return Object.keys(results).length === 1 502 | ? results[Object.keys(results)[0]] 503 | : results; 504 | } 505 | 506 | static filterResultsByWhereStatements(results, whereStatements) { 507 | if (!results) { 508 | return null; 509 | } 510 | let returnedResults = {}; 511 | let nonMatch = {}; 512 | for (let i = 0; i < whereStatements.length; i++) { 513 | let indexOffset = 1; 514 | let where = whereStatements[i]; 515 | const that = this; 516 | Object.keys(results).forEach(function(key, index) { 517 | let thisResult = results[key][where.field]; 518 | if (!that.conditionIsTrue(thisResult, where.value, where.comparator)) { 519 | nonMatch[key] = results[key]; 520 | } 521 | }); 522 | } 523 | if (nonMatch) { 524 | Object.keys(results).forEach(function(key, index) { 525 | if (!nonMatch[key]) { 526 | returnedResults[key] = results[key]; 527 | } 528 | }); 529 | return returnedResults; 530 | } else { 531 | return results; 532 | } 533 | } 534 | 535 | static conditionIsTrue(val1, val2, comparator) { 536 | switch (comparator) { 537 | case "=": 538 | return this.determineEquals(val1, val2); 539 | case "!=": 540 | return !this.determineEquals(val1, val2); 541 | case "<=": 542 | case "<": 543 | case ">=": 544 | case ">": 545 | return this.determineGreaterOrLess(val1, val2, comparator); 546 | case "like": 547 | return this.determineStringIsLike(val1, val2); 548 | case "!like": 549 | return !this.determineStringIsLike(val1, val2); 550 | default: 551 | throw "Unrecognized comparator: " + comparator; 552 | } 553 | } 554 | 555 | static determineEquals(val1, val2) { 556 | val1 = typeof val1 == "undefined" || val1 == "null" ? null : val1; 557 | val2 = typeof val2 == "undefined" || val2 == "null" ? null : val2; 558 | return val1 === val2; 559 | } 560 | 561 | static determineGreaterOrLess(val1, val2, comparator) { 562 | let isNum = false; 563 | if (isNaN(val1) || isNaN(val2)) { 564 | if (isValidDate(val1) && isValidDate(val2)) { 565 | return executeDateComparison(val1, val2, comparator); 566 | } 567 | } else { 568 | isNum = true; 569 | } 570 | switch (comparator) { 571 | case "<=": 572 | return isNum ? val1 <= val2 : val1.length <= val2.length; 573 | case ">=": 574 | return isNum ? val1 >= val2 : val1.length >= val2.length; 575 | case ">": 576 | return isNum ? val1 > val2 : val1.length < val2.length; 577 | case "<": 578 | return isNum ? val1 < val2 : val1.length < val2.length; 579 | } 580 | } 581 | 582 | static determineStringIsLike(val1, val2) { 583 | //TODO: LIKE fails on reserved regex characters (., +, etc) 584 | let regex = StringHelper.replaceAll(val2, "%", ".*"); 585 | regex = StringHelper.replaceAll(regex, "_", ".{1}"); 586 | // regex= StringHelper.replaceAll(regex,'\+','\+'); 587 | let re = new RegExp("^" + regex + "$", "g"); 588 | return re.test(val1); 589 | } 590 | 591 | static determineComparatorAndIndex(where) { 592 | let notEqIndex = this.getNotEqualIndex(where); 593 | if (notEqIndex >= 0) { 594 | return { comparator: "!=", index: notEqIndex }; 595 | } 596 | 597 | let greaterThanEqIndex = where.indexOf(">="); 598 | if (greaterThanEqIndex >= 0) { 599 | return { comparator: ">=", index: greaterThanEqIndex }; 600 | } 601 | 602 | let greaterThanIndex = where.indexOf(">"); 603 | if (greaterThanIndex >= 0) { 604 | return { comparator: ">", index: greaterThanIndex }; 605 | } 606 | 607 | let lessThanEqIndex = where.indexOf("<="); 608 | if (lessThanEqIndex >= 0) { 609 | return { comparator: "<=", index: lessThanEqIndex }; 610 | } 611 | let lessThanIndex = where.indexOf("<"); 612 | if (lessThanIndex >= 0) { 613 | return { comparator: "<", index: lessThanIndex }; 614 | } 615 | 616 | let notLikeIndex = where.toLowerCase().indexOf("!like"); 617 | if (notLikeIndex >= 0) { 618 | return { comparator: "!like", index: notLikeIndex }; 619 | } 620 | 621 | let likeIndex = where.toLowerCase().indexOf("like"); 622 | if (likeIndex >= 0) { 623 | return { comparator: "like", index: likeIndex }; 624 | } 625 | 626 | let eqIndex = where.indexOf("="); 627 | if (eqIndex >= 0) { 628 | return { comparator: "=", index: eqIndex }; 629 | } 630 | 631 | throw "Unrecognized comparator in where clause: '" + where + "'."; 632 | } 633 | 634 | static getInsertCount(query) { 635 | const splitQ = query.split(" "); 636 | if (splitQ[0].toUpperCase() === "INSERT" && parseInt(splitQ[1]) > 1) { 637 | return parseInt(splitQ[1]); 638 | } 639 | return 1; 640 | } 641 | 642 | static getNotEqualIndex(condition) { 643 | return StringHelper.regexIndexOf(condition, /!=|<>/); 644 | } 645 | 646 | static optimizeWheres(wheres) { 647 | //rearranges wheres so first statement is an equal, or error if no equals 648 | //firebase has no != method, so we'll grab whole collection, and filter on client 649 | const firstNotEqStatement = wheres[0]; 650 | for (let i = 0; i < wheres.length; i++) { 651 | if (wheres[i].value != null && wheres[i].comparator === "=") { 652 | wheres[0] = wheres[i]; 653 | wheres[i] = firstNotEqStatement; 654 | return wheres; 655 | } 656 | } 657 | 658 | wheres.unshift({ error: NO_EQUALITY_STATEMENTS }); 659 | return wheres; 660 | } 661 | } 662 | --------------------------------------------------------------------------------