Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0.3 #30

Merged
merged 43 commits into from
Dec 5, 2018
Merged

V0.3 #30

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
fd9f7f2
Use knex instead of js-data
zalmoxisus Nov 8, 2016
8b3a251
Merge pull request #29 from zalmoxisus/knex
zalmoxisus Nov 9, 2016
ee32bd5
Configure the maximum request body size
zalmoxisus Nov 9, 2016
9c048e6
Add HTTP request logging
zalmoxisus Nov 9, 2016
6f202e7
Add `payloads` schema
zalmoxisus Nov 9, 2016
4548209
Add `remotedev_users` schema and relations
zalmoxisus Nov 9, 2016
8419e7e
Use the current timestamps
zalmoxisus Nov 9, 2016
6794f63
Rename schema files
zalmoxisus Nov 9, 2016
1ba4d1e
Extend store API
zalmoxisus Nov 12, 2016
386d9ab
Hello GraphQL!
zalmoxisus Nov 12, 2016
8dbcb83
Dropping support for Node < 4 (#32)
jimmywarting Nov 14, 2016
84b962d
Fix typo (#33)
ratson Dec 20, 2016
bcc1d15
[Docs] Realtime monitoring
zalmoxisus Dec 28, 2016
46a61dc
[Test] Express backend
zalmoxisus Jan 2, 2017
aba4fcf
[Test] Realtime monitoring
zalmoxisus Jan 2, 2017
61a10f2
[Docs] Fix json loads
zalmoxisus Jan 2, 2017
b27fa50
[Test] Integration
zalmoxisus Jan 2, 2017
5732c37
Use in memory database by default
zalmoxisus Jan 2, 2017
95e7959
Downgrade knex
zalmoxisus Jan 2, 2017
12b0ac9
[Test] REST backend
zalmoxisus Jan 2, 2017
6390103
[Test] GraphQL backend
zalmoxisus Jan 2, 2017
bc8113c
Add info about C and C# client integrations
zalmoxisus Jan 19, 2017
0c007bc
Add details about connection path
zalmoxisus Jan 21, 2017
a0bbc23
Merge branch 'master' into v0.3
zalmoxisus Apr 14, 2017
073f27f
apollo-server -> graphql-server upgrade, node-uuid -> uuid, supertest…
modosc Apr 17, 2017
8cfa220
update graphql/graphql-server-express/graphql-tools
modosc Jul 10, 2017
e8b21bd
Merge branch 'master' into v0.3
zalmoxisus Jul 11, 2017
3687610
Merge pull request #41 from modosc/v0.3
zalmoxisus Jul 11, 2017
87b9a1c
v0.3.0-beta-6
zalmoxisus Jul 11, 2017
bda845a
Avoid error with MariaDB 10.0.30 (#43)
akaztp Jul 18, 2017
9c39052
fix database seeding second run.
akaztp Jul 24, 2017
d3d98da
Merge pull request #44 from akaztp/fix-db-cascade
zalmoxisus Jul 25, 2017
e9e2d5b
v0.3.0-beta-8
zalmoxisus Jul 25, 2017
6b7f0df
0.3.0-beta-9
zalmoxisus Sep 6, 2017
4a0c4c9
Merge branch 'master' into v0.3
zalmoxisus Aug 5, 2018
eab8d56
Fix payload truncated (#48)
akaztp Aug 5, 2018
008f980
Fix stored exception message (#49)
akaztp Aug 5, 2018
0d4b2d1
v0.3.0-beta-10
zalmoxisus Aug 5, 2018
0be73be
Merge branch 'master' into v0.3
zalmoxisus Dec 5, 2018
d68a756
Update socketcluster
zalmoxisus Dec 5, 2018
d90a668
Upgrade knex and fix sqlite config
zalmoxisus Dec 5, 2018
40ce580
Remove `js-data`
zalmoxisus Dec 5, 2018
1b26936
Update graphql
zalmoxisus Dec 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
.DS_Store
coverage
.idea
remotedev-db.sqlite3
13 changes: 13 additions & 0 deletions defaultDbOptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"client": "sqlite3",
"connection": { "filename": ":memory:" },
"pool": {
"min": 1,
"max": 1,
"idleTimeoutMillis": 360000000,
"disposeTimeout": 360000000
},
"useNullAsDefault": true,
"debug": false,
"migrate": true
}
155 changes: 155 additions & 0 deletions docs/API/Realtime.md
Original file line number Diff line number Diff line change
@@ -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));
```
8 changes: 3 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
var assign = require('object-assign');
var repeat = require('repeat-string');
var getPort = require('getport');
var SocketCluster = require('socketcluster');
var getOptions = require('./lib/options');

var LOG_LEVEL_NONE = 0;
Expand All @@ -9,8 +8,7 @@ var LOG_LEVEL_WARN = 2;
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
});
Expand All @@ -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));
}
Expand Down
5 changes: 0 additions & 5 deletions lib/adapter.js

This file was deleted.

21 changes: 21 additions & 0 deletions lib/api/schema.js
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions lib/api/schema_def.graphql
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions lib/db/connector.js
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions lib/db/migrations/index.js
Original file line number Diff line number Diff line change
@@ -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', 'longtext');
table.text('preloadedState', 'longtext');
table.text('screenshot', 'longtext');
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')
])
};
Loading