├── .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 | 
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 |
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 | | Date |
18 | Query |
19 | Committed |
20 |
21 |
22 |
23 | {history &&
24 | history.map((query, i) => {
25 | return (
26 |
27 | |
28 | {formatDate(query.date)}
29 | |
30 | store.appendQuery(query.body)}
35 | >
36 | {query.body.length <= queryTextLimit
37 | ? query.body
38 | : query.body.substring(0, queryTextLimit - 3) + "..."}
39 | |
40 |
41 | {query.committed && }
42 | |
43 | {query.body.length > queryTextLimit &&
44 |
50 |
51 | {StringHelper.getJsxWithNewLines(query.body)}
52 |
53 | }
54 |
55 | );
56 | })}
57 |
58 |
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 |
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 |
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 |
}
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 | ? | {
218 | e.stopPropagation();
219 | }}
220 | >
221 |
230 | this.props.setPathUnderEdit(null)}
233 | />
234 | |
235 | :
236 | {prop}
237 | | }
238 | {!this.props.noValue &&
239 |
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 | | }
271 | }
272 |
273 | );
274 | })}
275 |
276 |
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 |
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 |
--------------------------------------------------------------------------------