Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6b7f0dfd23 | ||
|
e9e2d5b1b0 | ||
|
d3d98daf7d | ||
|
9c39052ef0 | ||
|
bda845add5 | ||
|
87b9a1c5c7 | ||
|
36876104b6 | ||
|
e8b21bd556 | ||
|
8cfa2205b3 | ||
|
073f27f720 | ||
|
a0bbc23ef1 | ||
|
0c007bc1cb | ||
|
bc8113c7cf | ||
|
63901033d3 | ||
|
12b0ac9c74 | ||
|
95e79594e1 | ||
|
5732c3713b | ||
|
b27fa504ed | ||
|
61a10f2885 | ||
|
aba4fcf0fa | ||
|
46a61dc45e | ||
|
bcc1d15b84 | ||
|
84b962de19 | ||
|
8dbcb836ce | ||
|
386d9abfaf | ||
|
1ba4d1e40b | ||
|
6794f63629 | ||
|
8419e7e090 | ||
|
45482098d7 | ||
|
6f202e7715 | ||
|
9c048e66ef | ||
|
ee32bd52a6 | ||
|
8b3a251e20 | ||
|
fd9f7f2c0d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules
|
||||
.DS_Store
|
||||
coverage
|
||||
.idea
|
||||
remotedev-db.sqlite3
|
||||
|
6
defaultDbOptions.json
Normal file
6
defaultDbOptions.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"client": "sqlite3",
|
||||
"connection": { "filename": ":memory:" },
|
||||
"debug": false,
|
||||
"migrate": true
|
||||
}
|
155
docs/API/Realtime.md
Normal file
155
docs/API/Realtime.md
Normal 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));
|
||||
```
|
6
index.js
6
index.js
@ -1,5 +1,3 @@
|
||||
var assign = require('object-assign');
|
||||
var repeat = require('repeat-string');
|
||||
var getPort = require('getport');
|
||||
var getOptions = require('./lib/options');
|
||||
|
||||
@ -10,7 +8,7 @@ var LOG_LEVEL_INFO = 3;
|
||||
|
||||
module.exports = function(argv) {
|
||||
var SocketCluster = require('socketcluster').SocketCluster;
|
||||
var options = assign(getOptions(argv), {
|
||||
var options = Object.assign(getOptions(argv), {
|
||||
workerController: __dirname + '/lib/worker.js',
|
||||
allowClientPublish: false
|
||||
});
|
||||
@ -33,7 +31,7 @@ module.exports = function(argv) {
|
||||
} else {
|
||||
if (logLevel >= LOG_LEVEL_INFO) {
|
||||
console.log('[RemoteDev] Start server...');
|
||||
console.log(repeat('-', 80) + '\n');
|
||||
console.log('-'.repeat(80) + '\n');
|
||||
}
|
||||
resolve(new SocketCluster(options));
|
||||
}
|
||||
|
21
lib/api/schema.js
Normal file
21
lib/api/schema.js
Normal 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;
|
60
lib/api/schema_def.graphql
Normal file
60
lib/api/schema_def.graphql
Normal 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
27
lib/db/connector.js
Normal 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;
|
||||
};
|
71
lib/db/migrations/index.js
Normal file
71
lib/db/migrations/index.js
Normal 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
12
lib/db/seeds/index.js
Normal 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'
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
13
lib/middleware/graphiql.js
Normal file
13
lib/middleware/graphiql.js
Normal 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
13
lib/middleware/graphql.js
Normal 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
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
@ -1,4 +1,13 @@
|
||||
var path = require('path');
|
||||
|
||||
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 {
|
||||
host: argv.hostname || process.env.npm_package_remotedev_hostname || null,
|
||||
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,
|
||||
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,
|
||||
dbOptions: dbOptions,
|
||||
maxRequestBody: argv.passphrase || '16mb',
|
||||
logHTTPRequests: argv.logHTTPRequests,
|
||||
logLevel: argv.logLevel || 3
|
||||
};
|
||||
}
|
||||
|
91
lib/store.js
91
lib/store.js
@ -1,11 +1,10 @@
|
||||
var uuid = require('node-uuid');
|
||||
var uuid = require('uuid');
|
||||
var pick = require('lodash/pick');
|
||||
var JSData = require('js-data');
|
||||
var getAdapter = require('./adapter');
|
||||
var connector = require('./db/connector');
|
||||
|
||||
var store;
|
||||
var adapter;
|
||||
var Report;
|
||||
var reports = 'remotedev_reports';
|
||||
// var payloads = 'remotedev_payloads';
|
||||
var knex;
|
||||
|
||||
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) {
|
||||
return new Promise(function(resolve) {
|
||||
listEvery(query).then(function(data) {
|
||||
return resolve(map(data, fields || baseFields));
|
||||
});
|
||||
});
|
||||
var r = knex.select(fields || baseFields).from(reports);
|
||||
if (query) return r.where(query);
|
||||
return r;
|
||||
}
|
||||
|
||||
function listAll(query) {
|
||||
var r = knex.select().from(reports);
|
||||
if (query) return r.where(query);
|
||||
return r;
|
||||
}
|
||||
|
||||
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);
|
||||
return knex(reports).where('id', id).first();
|
||||
}
|
||||
|
||||
function add(data) {
|
||||
@ -60,8 +40,9 @@ function add(data) {
|
||||
return error('Type ' + data.type + ' is not supported yet.');
|
||||
}
|
||||
|
||||
var obj = {
|
||||
id: uuid.v4(),
|
||||
var reportId = uuid.v4();
|
||||
var report = {
|
||||
id: reportId,
|
||||
type: data.type,
|
||||
title: data.title || data.exception && data.exception.message || data.action,
|
||||
description: data.description,
|
||||
@ -70,22 +51,25 @@ function add(data) {
|
||||
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,
|
||||
instanceId: data.instanceId,
|
||||
meta: data.meta,
|
||||
exception: data.exception,
|
||||
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 new Promise(function(resolve) {
|
||||
var report = Report.inject(obj);
|
||||
return resolve(report);
|
||||
});
|
||||
}
|
||||
return Report.create(obj);
|
||||
return knex.insert(report).into(reports)
|
||||
.then(function (){ return byBaseFields(report); })
|
||||
}
|
||||
|
||||
function byBaseFields(data) {
|
||||
@ -93,24 +77,13 @@ function byBaseFields(data) {
|
||||
}
|
||||
|
||||
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');
|
||||
knex = connector(options);
|
||||
|
||||
return {
|
||||
list: list,
|
||||
listAll: listAll,
|
||||
get: get,
|
||||
add: add,
|
||||
selectors: {
|
||||
byBaseFields: byBaseFields
|
||||
}
|
||||
add: add
|
||||
};
|
||||
}
|
||||
|
||||
|
6
lib/utils/requireSchema.js
Normal file
6
lib/utils/requireSchema.js
Normal 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);
|
||||
};
|
@ -2,44 +2,68 @@ var path = require('path');
|
||||
var app = require('express')();
|
||||
var bodyParser = require('body-parser');
|
||||
var cors = require('cors');
|
||||
var morgan = require('morgan');
|
||||
var graphiqlMiddleware = require('./middleware/graphiql');
|
||||
var graphqlMiddleware = require('./middleware/graphql');
|
||||
var createStore = require('./store');
|
||||
|
||||
module.exports.run = function(worker) {
|
||||
var httpServer = worker.httpServer;
|
||||
var scServer = worker.scServer;
|
||||
var store = createStore(worker.options);
|
||||
var limit = worker.options.maxRequestBody;
|
||||
var logHTTPRequests = worker.options.logHTTPRequests;
|
||||
|
||||
httpServer.on('request', app);
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.resolve(__dirname, '..', 'views'));
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
app.use(cors({ methods: 'POST' }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json({ limit: limit }));
|
||||
app.use(bodyParser.urlencoded({ limit: limit, extended: false }));
|
||||
|
||||
app.use('/graphql', graphqlMiddleware(store));
|
||||
|
||||
app.post('/', function(req, res) {
|
||||
if (!req.body) return res.status(404).end();
|
||||
switch(req.body.op) {
|
||||
case 'get':
|
||||
store.get(req.body.id).then(function(r) {
|
||||
res.send(r || {});
|
||||
}).catch(function(error) {
|
||||
console.error(error);
|
||||
res.sendStatus(500)
|
||||
});
|
||||
break;
|
||||
case 'list':
|
||||
store.list(req.body.query, req.body.fields).then(function(r) {
|
||||
res.send(r);
|
||||
}).catch(function(error) {
|
||||
console.error(error);
|
||||
res.sendStatus(500)
|
||||
});
|
||||
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)
|
||||
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') {
|
||||
store.list().then(function(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) {
|
||||
store.get(id).then(function(data) {
|
||||
respond(null, data);
|
||||
}).catch(function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
socket.on('disconnect', function() {
|
||||
|
32
package.json
32
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotedev-server",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0-beta-9",
|
||||
"description": "Run the RemoteDev monitor on your local server.",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
@ -9,8 +9,15 @@
|
||||
"files": [
|
||||
"bin",
|
||||
"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": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zalmoxisus/remotedev-server.git"
|
||||
@ -19,6 +26,9 @@
|
||||
"devtools",
|
||||
"remotedev"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"author": "Mihail Diordiev <zalmoxisus@gmail.com> (https://github.com/zalmoxisus)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
@ -32,13 +42,23 @@
|
||||
"ejs": "^2.4.1",
|
||||
"express": "^4.13.3",
|
||||
"getport": "^0.1.0",
|
||||
"graphql": "^0.10.3",
|
||||
"graphql-server-express": "^1.0.0",
|
||||
"graphql-tools": "^1.1.0",
|
||||
"js-data": "^2.9.0",
|
||||
"knex": "0.11.10",
|
||||
"lodash": "^4.15.0",
|
||||
"minimist": "^1.2.0",
|
||||
"node-uuid": "^1.4.0",
|
||||
"object-assign": "^4.0.0",
|
||||
"repeat-string": "^1.5.4",
|
||||
"morgan": "^1.7.0",
|
||||
"semver": "^5.3.0",
|
||||
"socketcluster": "^6.7.1"
|
||||
"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
186
test/integration.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user