diff --git a/README.md b/README.md index a0c7c74..a9b0cfe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ RemoteDev Server ================ -Bridge for connecting [remotedev monitor app](https://github.com/zalmoxisus/remotedev-app) with [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) or [RemoteDev](https://github.com/zalmoxisus/remotedev) using a local server. Running a local server is optional, you may use [remotedev.io](https://remotedev.io) server instead, which is by default. +Bridge for communicating with an application remotely via [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension), [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) or [RemoteDev](https://github.com/zalmoxisus/remotedev). Running your server is optional, you can use [remotedev.io](https://remotedev.io) instead. ### Installation @@ -19,7 +19,7 @@ npm install --save-dev remotedev-server } ``` -So, you can start local server by running `npm run remotedev`. +So, you can start remotedev server by running `npm run remotedev`. ##### Import in your `server.js` script you use for starting a development server: @@ -36,7 +36,11 @@ So, you can start remotedev server together with your dev server. remotedev --hostname=localhost --port=8000 ``` -Change `hostname` and `port` to the values you want. +### Connection settings + +Set `hostname` and `port` to the values you want. `hostname` by default is `localhost` and `port` is `8000`. + +To use WSS, set `protocol` argument to `https` and provide `key`, `cert` and `passphrase` arguments. ### Inject to React Native local server @@ -50,9 +54,7 @@ Change `hostname` and `port` to the values you want. The `injectserver` value can be `reactnative` or `macos` ([react-native-macos](https://github.com/ptmt/react-native-macos)), it used `reactnative` by default. -Then, we can start React Native server and RemoteDev server with one command: - -![Inject server](https://cloud.githubusercontent.com/assets/3001525/16925822/92b6b3ac-4d58-11e6-9f36-d57dac8892c4.png) +Then, we can start React Native server and RemoteDev server with one command (`npm start`). ##### Revert the injection @@ -68,6 +70,8 @@ Or just run `$(npm bin)/remotedev --revert`. ### Connect from Android device or emulator +> Note that if you're using `injectserver` argument explained above, this step is not necessary. + If you're running an Android 5.0+ device connected via USB or an Android emulator, use [adb command line tool](http://developer.android.com/tools/help/adb.html) to setup port forwarding from the device to your computer: ``` @@ -76,6 +80,24 @@ adb reverse tcp:8000 tcp:8000 If you're still use Android 4.0, you should use `10.0.2.2` (Genymotion: `10.0.3.2`) instead of `localhost` in [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools#storeconfigurestorejs) or [remotedev](https://github.com/zalmoxisus/remotedev#usage). +### Save reports and logs + +You can store reports via [`redux-remotedev`](https://github.com/zalmoxisus/redux-remotedev) and get them replicated with [Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension) or [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools). You can get action history right in the extension just by clicking the link from a report. + +Remotedev server is database agnostic. By default everything is stored in the memory, but you can persist data by specifying one of the jsData adapters above for `adapter` argument. Also you can add an `dbOptions` argument for database configuration. If not provided the default options will be used (for some adapters, like `sql`, it's required). You have to install the required adapter's npm package. + +| Storage | `adapter` | `dbOptions` argument example (optional) | install | +|-----------|-----------|------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------| +| Firebase | firebase | `{ basePath: 'https://my-app.firebase.io' }` | `npm install --save js-data-firebase` | +| HTTP | http | `{ basePath: 'https://my-rest-server/api' }` | `npm install --save js-data-http` | +| LevelUp | levelup | `'./db'` (the levelup "db" object will be available at "adapter.db") | `npm install --save js-data-levelup` | +| MongoDB | mongodb | `{ name: 'user', idAttribute: '_id', table: 'users' }` | `npm install --save js-data-mongodb` | +| MySQL | sql | `{ client: 'mysql', connection: { host: '123.45.67.890', user: 'ubuntu', password: 'welcome1234', database: 'db1' }` | `npm install --save js-data-sql` | +| Postgres | sql | `{ client: 'pg', connection: { host: '123.45.67.890', user: 'ubuntu', password: 'welcome1234', database: 'db1' }` | `npm install --save js-data-sql` | +| Redis | redis | See the configurable options for [`node_redis`](https://github.com/NodeRedis/node_redis) | `npm install --save js-data-redis` | +| RethinkDB | rethinkdb | `{ host: '123.456.68.987', db: 'my_db' }` | `npm install --save rethinkdbdash js-data-rethinkdb` | +| SQLite3 | sql | `{ client: 'sqlite3', connection: { host: '123.45.67.890', user: 'ubuntu', password: 'welcome1234', database: 'db1' }` | `npm install --save js-data-sql` | + ### License MIT diff --git a/bin/remotedev.js b/bin/remotedev.js index 1ea3d55..4dca3aa 100755 --- a/bin/remotedev.js +++ b/bin/remotedev.js @@ -4,7 +4,7 @@ var path = require('path'); var argv = require('minimist')(process.argv.slice(2)); var chalk = require('chalk'); var injectServer = require('./injectServer'); -var getOptions = require('./../lib/getOptions'); +var getOptions = require('./../lib/options'); function readFile(filePath) { return fs.readFileSync(path.resolve(process.cwd(), filePath), 'utf-8'); diff --git a/bin/server.js b/bin/server.js index 12b722e..7837158 100644 --- a/bin/server.js +++ b/bin/server.js @@ -1,6 +1,6 @@ var assign = require('object-assign'); var repeat = require('repeat-string'); -var getOptions = require('./../lib/getOptions'); +var getOptions = require('./../lib/options'); var getPort = require('getport'); var LOG_LEVEL_NONE = 0; diff --git a/lib/adapter.js b/lib/adapter.js new file mode 100644 index 0000000..643e74d --- /dev/null +++ b/lib/adapter.js @@ -0,0 +1,5 @@ +function getAdapter(adapter) { + return require('js-data-' + adapter); +} + +module.exports = getAdapter; diff --git a/lib/getOptions.js b/lib/options.js similarity index 81% rename from lib/getOptions.js rename to lib/options.js index 682f829..b970e4b 100644 --- a/lib/getOptions.js +++ b/lib/options.js @@ -8,6 +8,8 @@ module.exports = function getOptions(argv) { cert: argv.cert || process.env.npm_package_remotedev_cert || null, passphrase: argv.passphrase || process.env.npm_package_remotedev_passphrase || null }, + adapter: argv.adapter || process.env.npm_package_remotedev_adapter, + dbOptions: argv.dbOptions || process.env.npm_package_remotedev_db, logLevel: argv.logLevel || 3 }; } diff --git a/lib/store.js b/lib/store.js new file mode 100644 index 0000000..b350c4d --- /dev/null +++ b/lib/store.js @@ -0,0 +1,117 @@ +var uuid = require('node-uuid'); +var pick = require('lodash/pick'); +var JSData = require('js-data'); +var getAdapter = require('./adapter'); + +var store; +var adapter; +var Report; + +var baseFields = ['id', 'title', 'added']; + +function error(msg) { + return new Promise(function(resolve, reject) { + return resolve({ error: msg }); + }); +} + +function map(data, fields) { + if (!fields) return data; + return data.map(function(r) { + return pick(r, fields); + }); +} + +function listEvery(query) { + if (!adapter) { + return new Promise(function(resolve) { + var report = Report.filter(query); + return resolve(report); + }); + } + return Report.findAll(query); +} + +function list(query, fields) { + return new Promise(function(resolve) { + listEvery(query).then(function(data) { + return resolve(map(data, fields || baseFields)); + }); + }); +} + +function get(id) { + if (!id) return error('No id specified.'); + + if (!adapter) { + return new Promise(function(resolve) { + var report = Report.get(id); + return resolve(report); + }); + } + return Report.find(id); +} + +function add(data) { + if (!data.type || !data.payload) { + return error('Required parameters aren\'t specified.'); + } + if (data.type !== 'ACTIONS' && data.type !== 'STATE') { + return error('Type ' + data.type + ' is not supported yet.'); + } + + var obj = { + id: uuid.v4(), + type: data.type, + title: data.title || data.exception && data.exception.message || data.action, + description: data.description, + action: data.action, + payload: data.payload, + preloadedState: data.preloadedState, + screenshot: data.screenshot, + version: data.version, + appId: data.appId, + userAgent: data.userAgent, + user: data.user, + userId: typeof data.user === 'object' ? data.user.id : data.user, + meta: data.meta, + exception: data.exception, + added: Date.now() + }; + + if (!adapter) { + return new Promise(function(resolve) { + var report = Report.inject(obj); + return resolve(report); + }); + } + return Report.create(obj); +} + +function byBaseFields(data) { + return pick(data, baseFields); +} + +function createStore(options) { + var adapterName = options.adapter; + store = new JSData.DS(); + + if (adapterName) { + var DSAdapter = getAdapter(adapterName); + adapter = new DSAdapter(options.dbOptions); + store.registerAdapter(adapterName, adapter, { default: true }); + } + + Report = store.defineResource('report'); + + return { + list: list, + get: get, + add: add, + selectors: { + byBaseFields: byBaseFields + } + }; +} + +module.exports = createStore; diff --git a/lib/worker.js b/lib/worker.js index 2a9e51a..238d755 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -2,10 +2,12 @@ var path = require('path'); var app = require('express')(); var bodyParser = require('body-parser'); var cors = require('cors'); +var createStore = require('./store'); module.exports.run = function(worker) { var httpServer = worker.httpServer; var scServer = worker.scServer; + var store = createStore(worker.options); httpServer.on('request', app); @@ -18,10 +20,28 @@ module.exports.run = function(worker) { app.use(cors({ methods: 'POST' })); app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: false })); app.post('/', function(req, res) { if (!req.body) return res.status(404).end(); - scServer.exchange.publish('log', req.body); - res.send('OK'); + switch(req.body.op) { + case 'get': + store.get(req.body.id).then(function(r) { + res.send(r || {}); + }); + break; + case 'list': + store.list(req.body.query, req.body.fields).then(function(r) { + res.send(r); + }); + break; + default: + store.add(req.body).then(function(r) { + res.send({ id: r.id, error: r.error }); + scServer.exchange.publish('report', { + type: 'add', data: store.selectors.byBaseFields(r) + }); + }); + } }); scServer.addMiddleware(scServer.MIDDLEWARE_EMIT, function (req, next) { @@ -35,6 +55,15 @@ module.exports.run = function(worker) { next(); }); + scServer.addMiddleware(scServer.MIDDLEWARE_SUBSCRIBE, function (req, next) { + next(); + if (req.channel === 'report') { + store.list().then(function(data) { + req.socket.emit(req.channel, { type: 'list', data: data }); + }); + } + }); + scServer.on('connection', function(socket) { var channelToWatch, channelToEmit; socket.on('login', function (credentials, respond) { @@ -48,6 +77,11 @@ module.exports.run = function(worker) { }); respond(null, channelToWatch); }); + socket.on('getReport', function (id, respond) { + store.get(id).then(function(data) { + respond(null, data); + }); + }); socket.on('disconnect', function() { var channel = worker.exchange.channel('sc-' + socket.id); channel.unsubscribe(); channel.destroy(); diff --git a/package.json b/package.json index 593668d..5712c41 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,10 @@ "ejs": "^2.4.1", "express": "^4.13.3", "getport": "^0.1.0", + "js-data": "^2.9.0", + "lodash": "^4.15.0", "minimist": "^1.2.0", + "node-uuid": "^1.4.0", "object-assign": "^4.0.0", "repeat-string": "^1.5.4", "semver": "^5.3.0",