47 Commits

Author SHA1 Message Date
6b7f0dfd23 0.3.0-beta-9 2017-09-06 21:52:07 +03:00
7bde44b110 v0.2.3 2017-09-06 21:49:37 +03:00
574fe1f02a Merge pull request #45 from skovhus/upgrade-socketcluster
Fix installation error caused by uws
2017-09-06 21:48:51 +03:00
8063a88990 Upgrade socketcluster dependency (uws@0.13.0 has been removed from npm) 2017-08-07 11:41:13 +02:00
e9e2d5b1b0 v0.3.0-beta-8 2017-07-25 22:10:28 +03:00
d3d98daf7d Merge pull request #44 from akaztp/fix-db-cascade
fix database seeding second run.
2017-07-25 22:06:51 +03:00
9c39052ef0 fix database seeding second run.
missing onDelete('cascade')
2017-07-24 09:41:26 +01:00
bda845add5 Avoid error with MariaDB 10.0.30 (#43)
Primary keys cannot be set after creating foreign keys.
2017-07-18 23:35:40 +03:00
87b9a1c5c7 v0.3.0-beta-6 2017-07-11 21:53:42 +03:00
36876104b6 Merge pull request #41 from modosc/v0.3
update graphql/graphql-server-express/graphql-tools
2017-07-11 21:51:38 +03:00
e8b21bd556 Merge branch 'master' into v0.3 2017-07-11 21:50:43 +03:00
c1d84f5ef9 v0.2.3 2017-07-11 21:47:22 +03:00
8cfa2205b3 update graphql/graphql-server-express/graphql-tools 2017-07-10 14:41:12 -07:00
58023a8d67 Support RN > 0.46.0-rc.0 for injectServer (#40) 2017-07-11 03:00:32 +08:00
073f27f720 apollo-server -> graphql-server upgrade, node-uuid -> uuid, supertest=2 -> supertest-3 (#39) 2017-04-17 12:00:43 +03:00
a0bbc23ef1 Merge branch 'master' into v0.3 2017-04-14 17:16:29 +03:00
38e81e3aad Support RN >= 0.44.0-rc.0 for injectserver (#38) 2017-04-14 17:10:53 +03:00
0c007bc1cb Add details about connection path
Related to
https://github.com/zalmoxisus/redux-devtools-extension/issues/288
2017-01-21 16:54:56 +02:00
bc8113c7cf Add info about C and C# client integrations 2017-01-19 22:53:48 +02:00
63901033d3 [Test] GraphQL backend 2017-01-02 18:49:28 +02:00
12b0ac9c74 [Test] REST backend 2017-01-02 18:45:10 +02:00
95e79594e1 Downgrade knex
Fix https://github.com/tgriesser/knex/issues/1701
2017-01-02 18:27:48 +02:00
5732c3713b Use in memory database by default 2017-01-02 18:01:09 +02:00
b27fa504ed [Test] Integration 2017-01-02 16:14:28 +02:00
61a10f2885 [Docs] Fix json loads 2017-01-02 16:13:19 +02:00
aba4fcf0fa [Test] Realtime monitoring 2017-01-02 16:12:39 +02:00
46a61dc45e [Test] Express backend 2017-01-02 14:42:44 +02:00
bcc1d15b84 [Docs] Realtime monitoring 2016-12-28 19:08:53 +02:00
84b962de19 Fix typo (#33) 2016-12-20 13:56:24 +02:00
8dbcb836ce Dropping support for Node < 4 (#32)
* Removed object-assign polypill
* According to http://node.green string#repeat is fine in v4
* removed repeat
2016-11-14 11:58:22 +02:00
386d9abfaf Hello GraphQL! 2016-11-12 16:21:41 +02:00
1ba4d1e40b Extend store API 2016-11-12 16:16:43 +02:00
6794f63629 Rename schema files 2016-11-09 20:13:19 +02:00
8419e7e090 Use the current timestamps 2016-11-09 20:12:28 +02:00
45482098d7 Add remotedev_users schema and relations 2016-11-09 19:59:02 +02:00
6f202e7715 Add payloads schema 2016-11-09 18:45:41 +02:00
9c048e66ef Add HTTP request logging
Related to #27.
2016-11-09 13:49:24 +02:00
ee32bd52a6 Configure the maximum request body size
Related to #28.
2016-11-09 12:54:42 +02:00
8b3a251e20 Merge pull request #29 from zalmoxisus/knex
Use knex instead of js-data
2016-11-09 12:28:47 +02:00
fd9f7f2c0d Use knex instead of js-data 2016-11-08 22:48:32 +02:00
7c106cd843 v0.2.1 2016-11-06 19:44:57 +02:00
3163b037fc Merge pull request #24 from zalmoxisus/pass-express-app
Refactor the module
2016-11-06 19:41:52 +02:00
60d81ae52b Use the same route for any url 2016-11-06 19:40:23 +02:00
d354edac1a Refactor the module 2016-11-06 12:20:41 +02:00
25419b6bf5 Add info about the docker image and making a custom adapter 2016-11-01 20:21:43 +02:00
5e281d1b94 v0.2.0 2016-09-11 18:56:53 +03:00
f3c050ac8e Merge pull request #20 from zalmoxisus/reports
Receive reports from users and get them replicated right in the extension
2016-09-11 18:55:39 +03:00
20 changed files with 688 additions and 82 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
.DS_Store .DS_Store
coverage coverage
.idea .idea
remotedev-db.sqlite3

View File

@ -9,6 +9,8 @@ Bridge for communicating with an application remotely via [Redux DevTools extens
npm install --save-dev remotedev-server npm install --save-dev remotedev-server
``` ```
Also [there's a docker image](https://github.com/jhen0409/docker-remotedev-server) you can use.
### Usage ### Usage
##### Add in your app's `package.json`: ##### Add in your app's `package.json`:
@ -98,6 +100,8 @@ Remotedev server is database agnostic. By default everything is stored in the me
| RethinkDB | rethinkdb | `{ host: '123.456.68.987', db: 'my_db' }` | `npm install --save rethinkdbdash js-data-rethinkdb` | | 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` | | SQLite3 | sql | `{ client: 'sqlite3', connection: { host: '123.45.67.890', user: 'ubuntu', password: 'welcome1234', database: 'db1' }` | `npm install --save js-data-sql` |
### License Implement a [custom adapter for JSData](http://www.js-data.io/docs/working-with-adapters#custom-adapters).
### License
MIT MIT

View File

@ -8,7 +8,9 @@ var endFlag = '/* ' + name + ' end */';
var serverFlags = { var serverFlags = {
'react-native': { 'react-native': {
'0.0.1': ' _server(argv, config, resolve, reject);', '0.0.1': ' _server(argv, config, resolve, reject);',
'0.31.0': " runServer(args, config, () => console.log('\\nReact packager ready.\\n'));" '0.31.0': " runServer(args, config, () => console.log('\\nReact packager ready.\\n'));",
'0.44.0-rc.0': ' runServer(args, config, startedCallback, readyCallback);',
'0.46.0-rc.0': ' runServer(runServerArgs, configT, startedCallback, readyCallback);'
}, },
'react-native-desktop': { 'react-native-desktop': {
'0.0.1': ' _server(argv, config, resolve, reject);' '0.0.1': ' _server(argv, config, resolve, reject);'
@ -92,4 +94,4 @@ exports.revert = function(modulePath, moduleName) {
); );
} }
return true; return true;
}; };

View File

@ -70,4 +70,4 @@ if (argv.injectserver) {
process.exit(pass ? 0 : 1); process.exit(pass ? 0 : 1);
} }
require('./server')(argv); require('../index')(argv);

6
defaultDbOptions.json Normal file
View File

@ -0,0 +1,6 @@
{
"client": "sqlite3",
"connection": { "filename": ":memory:" },
"debug": false,
"migrate": true
}

155
docs/API/Realtime.md Normal file
View File

@ -0,0 +1,155 @@
## Realtime monitoring
### WebSocket Clients
We're using [SocketCluster](http://socketcluster.io/) for realtime communication, which provides a fast and scalable webSocket layer (via [`µWS`](https://github.com/uWebSockets/uWebSockets)) and a minimal pub/sub system. You need to include one of [its clients](https://github.com/SocketCluster/client-drivers) in your app to communicate with RemotedevServer. Currently there are clients for [JavaScript (NodeJS)](https://github.com/SocketCluster/socketcluster-client), [Java](https://github.com/sacOO7/socketcluster-client-java), [Python](https://github.com/sacOO7/socketcluster-client-python), [C](https://github.com/sacOO7/socketcluster-client-C), [Objective-C](https://github.com/abpopov/SocketCluster-ios-client) and [.NET/C#](https://github.com/sacOO7/SocketclusterClientDotNet).
By default, the websocket server is running on `ws://localhost:8000/socketcluster/`.
### Messaging lifecycle
#### 1. Connecting to the WebSocket server
The client driver provides a way to connect to the server via websockets (see the docs for the selected client).
##### JavaScript
```js
var socket = socketCluster.connect({
hostname: 'localhost',
port: 8000
});
```
##### Python
```py
socket = Socketcluster.socket("ws://localhost:8000/socketcluster/")
socket.connect()
```
> Note that JavaScript client composes the url from `hostname` and `port`, adding `/socketcluster/` path automatically. For other clients, you should specify that path. For example, for `ObjectiveC` it would be `self.client.initWithHost("localhost/socketcluster/", onPort: 8000, securely: false)`.
#### 2. Disconnecting and reconnecting
SocketCluster client handles reconnecting for you, but you still might want to know when the connection is established, or when it failed to connect.
##### JavaScript
```js
socket.on('connect', status => {
// Here will come the next step
});
socket.on('disconnect', code => {
console.warn('Socket disconnected with code', code);
});
socket.on('error', error => {
console.warn('Socket error', error);
});
```
##### Python
```py
def onconnect(socket):
// Here will call the next step
def ondisconnect(socket):
logging.info("on disconnect got called")
def onConnectError(socket, error):
logging.info("On connect error got called")
socket.setBasicListener(onconnect, ondisconnect, onConnectError)
```
#### 3. Authorizing and subscribing to the channel of events
We're not providing an authorizing mechanism yet. All you have to do is to emit a `login` event, and you'll get a `channelName` you should subscribe for, and watch for messages and events. Make sure to pass the `master` event, otherwise it should be a monitor, not a client app.
##### JavaScript
```js
socket.emit('login', 'master', (error, channelName) => {
if (error) { console.log(error); return; }
channel = socket.subscribe(channelName);
channel.watch(handleMessages);
socket.on(channelName, handleMessages);
});
function handleMessages(message) {
// 5. Listening for monitor events
}
```
##### Python
```py
socket.emitack("login", "master", login)
def login(key, error, channelName):
socket.subscribe(channelName)
socket.onchannel(channelName, handleMessages)
socket.on(channelName, handleMessages)
def handleMessages(key, message):
// 5. Listening for monitor events
```
You could just emit the `login` event, and omit subscribing (and point `5` bellow) if you want only to log data, not to interact with te app.
#### 4. Sending the action and state to the monitor
To send your data to the monitor use `log` or `log-noid` channel. The latter will add the socket id to the message from the server side (useful when the message was sent before the connection was established).
The message object includes the following:
- `type` - usually should be `ACTION`. If you want to indicate that we're starting a new log (clear all actions emitted before and add `@@INIT`), use `INIT`. In case you have a lifted state similar to one provided by [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument), use `STATE`.
- `action` - the action object. It is recommended to lift it in another object, and add `timestamp` to show when the action was fired off: `{ timestamp: Date.now(), action: { type: 'SOME_ACTION' } }`.
- `payload` - usually the state or lifted state object.
- `name` - name of the instance to be shown in the instances selector. If not provided, it will be equal to `instanceId`.
- `instanceId` - an id to identify the instance. If not provided, it will be the same as `id`. However, it is useful when having several instances (or stores) in the same connection. Also if the user will specify a constant value, it would allow to persist the state on app reload.
- `id` - socket connection id, which should be either `socket.id` or should not provided and use `log-noid` channel.
##### JavaScript
```js
const message = {
type: 'ACTION',
action: { action, timestamp: Date.now() },
payload: state,
id: socket.id,
instanceId: window.btoa(location.href),
name: document.title
};
socket.emit(socket.id ? 'log' : 'log-noid', message);
```
##### Python
```py
class Message:
def __init__(self, action, state):
self.type = "ACTION"
self.action = action
self.payload = state
id: socket.id
socket.emit(socket.id if "log" else "log-noid", Message(action, state));
```
#### 5. Listening for monitor events
When a monitor action is emitted, you'll get an event on the subscribed function. The argument object includes a `type` key, which can be:
- `DISPATCH` - a monitor action dispatched on Redux DevTools monitor, like `{ type: 'DISPATCH', payload: { type: 'JUMP_TO_STATE', 'index': 2 }`. See [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js) for details. Additionally to that API, you'll get also a stringified `state` object when needed. So, for example, for time travelling (`JUMP_TO_STATE`) you can just parse and set the state (see the example). Usually implementing this type of actions would be enough.
- `ACTION` - the user requested to dispatch an action remotely like `{ type: 'ACTION', action: '{ type: \'INCREMENT_COUNTER\' }' }`. The `action` can be either a stringified javascript object which should be evalled or a function which arguments should be evalled like [here](https://github.com/zalmoxisus/remotedev-utils/blob/master/src/index.js#L62-L70).
- `START` - a monitor was opened. You could handle this event in order not to do extra tasks when the app is not monitored.
- `STOP` - a monitor was closed. You can take this as no need to send data to the monitor. I there are several monitors and one was closed, all others will send `START` event to acknowledge that we still have to send data.
See [`mobx-remotedev`](https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js) for an example of implementation without [`redux-devtools-instrument`](https://github.com/zalmoxisus/redux-devtools-instrument/blob/master/src/instrument.js).
##### JavaScript
```js
function handleMessages(message) {
if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_STATE') {
store.setState(JSON.parse(message.state));
}
}
```
##### Python
```py
def handleMessages(key, message):
if message.type === "DISPATCH" and message.payload.type === "JUMP_TO_STATE":
store.setState(json.loads(message.state));
```

View File

@ -1,7 +1,5 @@
var assign = require('object-assign');
var repeat = require('repeat-string');
var getOptions = require('./../lib/options');
var getPort = require('getport'); var getPort = require('getport');
var getOptions = require('./lib/options');
var LOG_LEVEL_NONE = 0; var LOG_LEVEL_NONE = 0;
var LOG_LEVEL_ERROR = 1; var LOG_LEVEL_ERROR = 1;
@ -10,8 +8,8 @@ var LOG_LEVEL_INFO = 3;
module.exports = function(argv) { module.exports = function(argv) {
var SocketCluster = require('socketcluster').SocketCluster; var SocketCluster = require('socketcluster').SocketCluster;
var options = assign(getOptions(argv), { var options = Object.assign(getOptions(argv), {
workerController: __dirname + '/../lib/worker.js', workerController: __dirname + '/lib/worker.js',
allowClientPublish: false allowClientPublish: false
}); });
var port = options.port; var port = options.port;
@ -33,7 +31,7 @@ module.exports = function(argv) {
} else { } else {
if (logLevel >= LOG_LEVEL_INFO) { if (logLevel >= LOG_LEVEL_INFO) {
console.log('[RemoteDev] Start server...'); console.log('[RemoteDev] Start server...');
console.log(repeat('-', 80) + '\n'); console.log('-'.repeat(80) + '\n');
} }
resolve(new SocketCluster(options)); resolve(new SocketCluster(options));
} }

21
lib/api/schema.js Normal file
View File

@ -0,0 +1,21 @@
var makeExecutableSchema = require('graphql-tools').makeExecutableSchema;
var requireSchema = require('../utils/requireSchema');
var schema = requireSchema('./schema_def.graphql', require);
var resolvers = {
Query: {
reports: function report(source, args, context, ast) {
return context.store.listAll();
},
report: function report(source, args, context, ast) {
return context.store.get(args.id);
}
}
};
var executableSchema = makeExecutableSchema({
typeDefs: schema,
resolvers: resolvers
});
module.exports = executableSchema;

View File

@ -0,0 +1,60 @@
# A list of options for the type of the report
enum ReportType {
STATE
ACTION
STATES
ACTIONS
}
type Report {
# Report ID
id: ID!
# Type of the report, can be: STATE, ACTION, STATES, ACTIONS
type: ReportType,
# Briefly what happened
title: String,
# Details supplied by the user
description: String,
# The last dispatched action before the report was sent
action: String,
# Stringified actions or the state or both, which should be loaded the application to reproduce the exact behavior
payload: String,
# Stringified preloaded state object. Could be the initial state of the app or committed state (after dispatching COMMIT action or reaching maxAge)
preloadedState: String,
# Screenshot url or blob as a string
screenshot: String,
# User Agent String
userAgent: String,
# Application version to group the reports and versioning
version: String,
# Used to identify the user who sent the report
userId: String,
# More detailed data about the user, usually it's a stringified object
user: String,
# Everything else you want to send
meta: String,
# Error message which invoked sending the report
exception: String,
# Id to identify the store in case there are multiple stores
instanceId: String,
# Timestamp when the report was added
added: String
# Id to identify the application (from apps table)
appId: ID
}
# Explore GraphQL query schema
type Query {
# List all reports
reports: [Report]
# Get a report by ID
report(
# Report ID
id: ID!
): Report
}
schema {
query: Query
#mutation: Mutation
}

27
lib/db/connector.js Normal file
View File

@ -0,0 +1,27 @@
var path = require('path');
var knexModule = require('knex');
module.exports = function connector(options) {
var dbOptions = options.dbOptions;
dbOptions.useNullAsDefault = true;
if (!dbOptions.migrate) {
return knexModule(dbOptions);
}
dbOptions.migrations = { directory: path.resolve(__dirname, 'migrations') };
dbOptions.seeds = { directory: path.resolve(__dirname, 'seeds') };
var knex = knexModule(dbOptions);
knex.migrate.latest()
.then(function() {
return knex.seed.run();
})
.then(function() {
console.log(' \x1b[0;32m[Done]\x1b[0m Migrations are finished\n');
})
.catch(function(error) {
console.error(error);
});
return knex;
};

View File

@ -0,0 +1,71 @@
exports.up = function(knex, Promise) {
return Promise.all([
knex.schema.createTable('remotedev_reports', function(table) {
table.uuid('id').primary();
table.string('type');
table.string('title');
table.string('description');
table.string('action');
table.text('payload');
table.text('preloadedState');
table.text('screenshot');
table.string('userAgent');
table.string('version');
table.string('user');
table.string('userId');
table.string('instanceId');
table.string('meta');
table.string('exception');
table.timestamp('added').defaultTo(knex.fn.now());
table.uuid('appId')
.references('id')
.inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE')
.defaultTo('78626c31-e16b-4528-b8e5-f81301b627f4');
}),
knex.schema.createTable('remotedev_payloads', function(table){
table.uuid('id').primary();
table.text('state');
table.text('action');
table.timestamp('added').defaultTo(knex.fn.now());
table.uuid('reportId')
.references('id')
.inTable('remotedev_reports').onDelete('CASCADE').onUpdate('CASCADE');
}),
knex.schema.createTable('remotedev_apps', function(table){
table.uuid('id').primary();
table.string('title');
table.string('description');
table.string('url');
table.timestamps(false, true);
}),
knex.schema.createTable('remotedev_users', function(table){
table.uuid('id').primary();
table.string('name');
table.string('login');
table.string('email');
table.string('avatarUrl');
table.string('profileUrl');
table.string('oauthId');
table.string('oauthType');
table.string('token');
table.timestamps(false, true);
}),
knex.schema.createTable('remotedev_users_apps', function(table){
table.boolean('readOnly').defaultTo(false);
table.uuid('userId');
table.uuid('appId');
table.primary(['userId', 'appId']);
table.foreign('userId')
.references('id').inTable('remotedev_users').onDelete('CASCADE').onUpdate('CASCADE');
table.foreign('appId')
.references('id').inTable('remotedev_apps').onDelete('CASCADE').onUpdate('CASCADE');
})
])
};
exports.down = function(knex, Promise) {
return Promise.all([
knex.schema.dropTable('remotedev_reports'),
knex.schema.dropTable('remotedev_apps')
])
};

12
lib/db/seeds/index.js Normal file
View File

@ -0,0 +1,12 @@
exports.seed = function(knex, Promise) {
return Promise.all([
knex('remotedev_apps').del()
]).then(function() {
return Promise.all([
knex('remotedev_apps').insert({
id: '78626c31-e16b-4528-b8e5-f81301b627f4',
title: 'Default'
})
]);
});
};

View File

@ -0,0 +1,13 @@
var graphiqlExpress = require('graphql-server-express').graphiqlExpress;
module.exports = graphiqlExpress({
endpointURL: '/graphql',
query:
'{\n' +
' reports {\n' +
' id,\n' +
' type,\n' +
' title\n' +
' }\n' +
'}'
});

13
lib/middleware/graphql.js Normal file
View File

@ -0,0 +1,13 @@
var graphqlExpress = require('graphql-server-express').graphqlExpress;
var schema = require('../api/schema');
module.exports = function (store) {
return graphqlExpress(function() {
return {
schema: schema,
context: {
store: store
}
};
});
};

View File

@ -1,4 +1,13 @@
var path = require('path');
module.exports = function getOptions(argv) { module.exports = function getOptions(argv) {
var dbOptions = argv.dbOptions;
if (typeof dbOptions === 'string') {
dbOptions = require(path.resolve(process.cwd(), argv.dbOptions));
} else if (typeof dbOptions === 'undefined') {
dbOptions = require('../defaultDbOptions.json');
}
return { return {
host: argv.hostname || process.env.npm_package_remotedev_hostname || null, host: argv.hostname || process.env.npm_package_remotedev_hostname || null,
port: Number(argv.port || process.env.npm_package_remotedev_port) || 8000, port: Number(argv.port || process.env.npm_package_remotedev_port) || 8000,
@ -8,8 +17,9 @@ module.exports = function getOptions(argv) {
cert: argv.cert || process.env.npm_package_remotedev_cert || null, cert: argv.cert || process.env.npm_package_remotedev_cert || null,
passphrase: argv.passphrase || process.env.npm_package_remotedev_passphrase || null passphrase: argv.passphrase || process.env.npm_package_remotedev_passphrase || null
}, },
adapter: argv.adapter || process.env.npm_package_remotedev_adapter, dbOptions: dbOptions,
dbOptions: argv.dbOptions || process.env.npm_package_remotedev_db, maxRequestBody: argv.passphrase || '16mb',
logHTTPRequests: argv.logHTTPRequests,
logLevel: argv.logLevel || 3 logLevel: argv.logLevel || 3
}; };
} }

View File

@ -1,11 +1,10 @@
var uuid = require('node-uuid'); var uuid = require('uuid');
var pick = require('lodash/pick'); var pick = require('lodash/pick');
var JSData = require('js-data'); var connector = require('./db/connector');
var getAdapter = require('./adapter');
var store; var reports = 'remotedev_reports';
var adapter; // var payloads = 'remotedev_payloads';
var Report; var knex;
var baseFields = ['id', 'title', 'added']; var baseFields = ['id', 'title', 'added'];
@ -15,41 +14,22 @@ function 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) { function list(query, fields) {
return new Promise(function(resolve) { var r = knex.select(fields || baseFields).from(reports);
listEvery(query).then(function(data) { if (query) return r.where(query);
return resolve(map(data, fields || baseFields)); return r;
}); }
});
function listAll(query) {
var r = knex.select().from(reports);
if (query) return r.where(query);
return r;
} }
function get(id) { function get(id) {
if (!id) return error('No id specified.'); if (!id) return error('No id specified.');
if (!adapter) { return knex(reports).where('id', id).first();
return new Promise(function(resolve) {
var report = Report.get(id);
return resolve(report);
});
}
return Report.find(id);
} }
function add(data) { function add(data) {
@ -60,8 +40,9 @@ function add(data) {
return error('Type ' + data.type + ' is not supported yet.'); return error('Type ' + data.type + ' is not supported yet.');
} }
var obj = { var reportId = uuid.v4();
id: uuid.v4(), var report = {
id: reportId,
type: data.type, type: data.type,
title: data.title || data.exception && data.exception.message || data.action, title: data.title || data.exception && data.exception.message || data.action,
description: data.description, description: data.description,
@ -70,22 +51,25 @@ function add(data) {
preloadedState: data.preloadedState, preloadedState: data.preloadedState,
screenshot: data.screenshot, screenshot: data.screenshot,
version: data.version, version: data.version,
appId: data.appId,
userAgent: data.userAgent, userAgent: data.userAgent,
user: data.user, user: data.user,
userId: typeof data.user === 'object' ? data.user.id : data.user, userId: typeof data.user === 'object' ? data.user.id : data.user,
instanceId: data.instanceId,
meta: data.meta, meta: data.meta,
exception: data.exception, exception: data.exception,
added: Date.now() added: Date.now()
}; };
if (data.appId) report.appId = data.appId; // TODO check if the id exists and we have access to link it
/*
var payload = {
id: uuid.v4(),
reportId: reportId,
state: data.payload
};
*/
if (!adapter) { return knex.insert(report).into(reports)
return new Promise(function(resolve) { .then(function (){ return byBaseFields(report); })
var report = Report.inject(obj);
return resolve(report);
});
}
return Report.create(obj);
} }
function byBaseFields(data) { function byBaseFields(data) {
@ -93,24 +77,13 @@ function byBaseFields(data) {
} }
function createStore(options) { function createStore(options) {
var adapterName = options.adapter; knex = connector(options);
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 { return {
list: list, list: list,
listAll: listAll,
get: get, get: get,
add: add, add: add
selectors: {
byBaseFields: byBaseFields
}
}; };
} }

View File

@ -0,0 +1,6 @@
var fs = require('fs');
module.exports = function(name, require) {
return fs.readFileSync(require.resolve(name)).toString();
// return GraphQL.buildSchema(schema);
};

View File

@ -2,44 +2,68 @@ var path = require('path');
var app = require('express')(); var app = require('express')();
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var cors = require('cors'); var cors = require('cors');
var morgan = require('morgan');
var graphiqlMiddleware = require('./middleware/graphiql');
var graphqlMiddleware = require('./middleware/graphql');
var createStore = require('./store'); var createStore = require('./store');
module.exports.run = function(worker) { module.exports.run = function(worker) {
var httpServer = worker.httpServer; var httpServer = worker.httpServer;
var scServer = worker.scServer; var scServer = worker.scServer;
var store = createStore(worker.options); var store = createStore(worker.options);
var limit = worker.options.maxRequestBody;
var logHTTPRequests = worker.options.logHTTPRequests;
httpServer.on('request', app); httpServer.on('request', app);
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('views', path.resolve(__dirname, '..', 'views')); app.set('views', path.resolve(__dirname, '..', 'views'));
app.get('/', function(req, res) { if (logHTTPRequests) {
if (typeof logHTTPRequests === 'object') app.use(morgan('combined', logHTTPRequests));
else app.use(morgan('combined'));
}
app.use('/graphiql', graphiqlMiddleware);
app.get('*', function(req, res) {
res.render('index', { port: worker.options.port }); res.render('index', { port: worker.options.port });
}); });
app.use(cors({ methods: 'POST' })); app.use(cors({ methods: 'POST' }));
app.use(bodyParser.json()); app.use(bodyParser.json({ limit: limit }));
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ limit: limit, extended: false }));
app.use('/graphql', graphqlMiddleware(store));
app.post('/', function(req, res) { app.post('/', function(req, res) {
if (!req.body) return res.status(404).end(); if (!req.body) return res.status(404).end();
switch(req.body.op) { switch(req.body.op) {
case 'get': case 'get':
store.get(req.body.id).then(function(r) { store.get(req.body.id).then(function(r) {
res.send(r || {}); res.send(r || {});
}).catch(function(error) {
console.error(error);
res.sendStatus(500)
}); });
break; break;
case 'list': case 'list':
store.list(req.body.query, req.body.fields).then(function(r) { store.list(req.body.query, req.body.fields).then(function(r) {
res.send(r); res.send(r);
}).catch(function(error) {
console.error(error);
res.sendStatus(500)
}); });
break; break;
default: default:
store.add(req.body).then(function(r) { store.add(req.body).then(function(r) {
res.send({ id: r.id, error: r.error }); res.send({ id: r.id, error: r.error });
scServer.exchange.publish('report', { scServer.exchange.publish('report', {
type: 'add', data: store.selectors.byBaseFields(r) type: 'add', data: r
}); });
}).catch(function(error) {
console.error(error);
res.status(500).send({})
}); });
} }
}); });
@ -60,6 +84,8 @@ module.exports.run = function(worker) {
if (req.channel === 'report') { if (req.channel === 'report') {
store.list().then(function(data) { store.list().then(function(data) {
req.socket.emit(req.channel, { type: 'list', data: data }); req.socket.emit(req.channel, { type: 'list', data: data });
}).catch(function(error) {
console.error(error);
}); });
} }
}); });
@ -80,6 +106,8 @@ module.exports.run = function(worker) {
socket.on('getReport', function (id, respond) { socket.on('getReport', function (id, respond) {
store.get(id).then(function(data) { store.get(id).then(function(data) {
respond(null, data); respond(null, data);
}).catch(function(error) {
console.error(error);
}); });
}); });
socket.on('disconnect', function() { socket.on('disconnect', function() {

View File

@ -1,16 +1,23 @@
{ {
"name": "remotedev-server", "name": "remotedev-server",
"version": "0.1.7", "version": "0.3.0-beta-9",
"description": "Run the RemoteDev monitor on your local server.", "description": "Run the RemoteDev monitor on your local server.",
"main": "bin/server.js", "main": "index.js",
"bin": { "bin": {
"remotedev": "bin/remotedev.js" "remotedev": "bin/remotedev.js"
}, },
"files": [ "files": [
"bin", "bin",
"lib", "lib",
"views" "views",
"index.js",
"defaultDbOptions.json"
], ],
"scripts": {
"test": "NODE_ENV=test mocha --recursive",
"test:watch": "NODE_ENV=test mocha --recursive --watch",
"prepublish": "npm run test"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/zalmoxisus/remotedev-server.git" "url": "https://github.com/zalmoxisus/remotedev-server.git"
@ -19,6 +26,9 @@
"devtools", "devtools",
"remotedev" "remotedev"
], ],
"engines": {
"node": ">=4.0.0"
},
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)", "author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
@ -32,13 +42,23 @@
"ejs": "^2.4.1", "ejs": "^2.4.1",
"express": "^4.13.3", "express": "^4.13.3",
"getport": "^0.1.0", "getport": "^0.1.0",
"graphql": "^0.10.3",
"graphql-server-express": "^1.0.0",
"graphql-tools": "^1.1.0",
"js-data": "^2.9.0", "js-data": "^2.9.0",
"knex": "0.11.10",
"lodash": "^4.15.0", "lodash": "^4.15.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"node-uuid": "^1.4.0", "morgan": "^1.7.0",
"object-assign": "^4.0.0",
"repeat-string": "^1.5.4",
"semver": "^5.3.0", "semver": "^5.3.0",
"socketcluster": "^5.0.4" "socketcluster": "^6.7.1",
"sqlite3": "^3.1.8",
"uuid": "^3.0.1"
},
"devDependencies": {
"expect": "^1.20.2",
"mocha": "^3.2.0",
"socketcluster-client": "^5.1.1",
"supertest": "^3.0.0"
} }
} }

186
test/integration.spec.js Normal file
View File

@ -0,0 +1,186 @@
var childProcess = require('child_process');
var request = require('supertest');
var expect = require('expect');
var scClient = require('socketcluster-client');
var remotedev = require('../');
describe('Server', function() {
var scServer;
this.timeout(5000);
before(function(done) {
scServer = childProcess.fork(__dirname + '/../bin/remotedev.js');
setTimeout(done, 2000);
});
after(function() {
if (scServer) {
scServer.kill();
}
});
describe('Express backend', function() {
it('loads main page', function() {
request('http://localhost:8000')
.get('/')
.expect('Content-Type', /text\/html/)
.expect(200)
.then(function(res) {
expect(res.text).toMatch(/<title>RemoteDev<\/title>/);
})
});
it('resolves an inexistent url', function(done) {
request('http://localhost:8000/jreerfr/123')
.get('/')
.expect('Content-Type', /text\/html/)
.expect(200, done);
});
});
describe('Realtime monitoring', function() {
var socket, socket2, channel;
before(function() {
socket = scClient.connect({ hostname: 'localhost', port: 8000 });
socket.connect();
socket.on('error', function(error) {
console.error('Socket1 error', error);
});
socket2 = scClient.connect({ hostname: 'localhost', port: 8000 });
socket2.connect();
socket.on('error', function(error) {
console.error('Socket2 error', error);
});
});
after(function() {
socket.disconnect();
socket2.disconnect();
});
it('should connect', function(done) {
socket.on('connect', function(status) {
expect(status.id).toExist();
done();
});
});
it('should login', function() {
socket.emit('login', 'master', function(error, channelName) {
if (error) { console.log(error); return; }
expect(channelName).toBe('respond');
channel = socket.subscribe(channelName);
expect(channel.SUBSCRIBED).toBe('subscribed');
});
});
it('should send message', function(done) {
var data = {
"type": "ACTION",
"payload": {
"todos": "do some"
},
"action": {
"timestamp": 1483349708506,
"action": {
"type": "ADD_TODO",
"text": "hggg"
}
},
"instanceId": "tAmA7H5fclyWhvizAAAi",
"name": "LoggerInstance",
"id": "tAmA7H5fclyWhvizAAAi"
};
socket2.emit('login', '', function(error, channelName) {
if (error) { console.log(error); return; }
expect(channelName).toBe('log');
var channel2 = socket2.subscribe(channelName);
expect(channel2.SUBSCRIBED).toBe('subscribed');
channel2.on('subscribe', function() {
channel2.watch(function(message) {
expect(message).toEqual(data);
done();
});
socket.emit(channelName, data);
})
});
});
});
describe('REST backend', function() {
var id;
var report = {
type: 'ACTIONS',
title: 'Test report',
description: 'Test body report',
action: 'SOME_FINAL_ACTION',
payload: '[{"type":"ADD_TODO","text":"hi"},{"type":"SOME_FINAL_ACTION"}]',
preloadedState: '{"todos":[{"text":"Use Redux","completed":false,"id":0}]}',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36'
};
it('should add a report', function() {
request('http://localhost:8000')
.post('/')
.send(report)
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
id = res.body.id;
expect(id).toExist();
});
});
it('should get the report', function() {
request('http://localhost:8000')
.post('/')
.send({
op: 'get',
id: id
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
expect(res.body).toInclude(report);
});
});
it('should list reports', function() {
request('http://localhost:8000')
.post('/')
.send({
op: 'list'
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
expect(res.body.length).toBe(1);
expect(res.body[0].id).toBe(id);
expect(res.body[0].title).toBe('Test report');
expect(res.body[0].added).toExist();
});
});
});
describe('GraphQL backend', function() {
it('should get the report', function() {
request('http://localhost:8000')
.post('/graphql')
.send({
query: '{ reports { id, type, title } }'
})
.set('Accept', 'application/json')
.expect('Content-Type', /application\/json/)
.expect(200)
.then(function(res) {
var reports = res.body.data.reports;
expect(reports.length).toBe(1);
expect(reports[0].id).toExist();
expect(reports[0].title).toBe('Test report');
expect(reports[0].type).toBe('ACTIONS');
});
});
});
});