gremlin-client
Advanced tools
Comparing version 0.2.1 to 0.3.0
@@ -0,1 +1,6 @@ | ||
## 0.3.0 | ||
- `client.stream()` now re-emits one distinct `data` event per result fetched instead of emiting an array of results | ||
- `client.execute()` now internally uses a stream to buffer partial results before firing the provided callback | ||
- Add `client.messageStream()` which returns a stream of raw response messages returned by Gremlin Server | ||
## 0.2.1 | ||
@@ -2,0 +7,0 @@ - Update dependencies |
@@ -8,7 +8,7 @@ $(function() { | ||
var script = 'g.V'; | ||
var script = 'g.V()'; | ||
var query = client.stream(script); | ||
query.on('data', function(d) { | ||
$("#results").append('<li>'+ JSON.stringify(d[0]) +'</li>'); | ||
$("#results").append('<li>'+ JSON.stringify(d) +'</li>'); | ||
}); | ||
@@ -15,0 +15,0 @@ |
@@ -10,4 +10,4 @@ var gremlin = require('../'); | ||
g.v(id).out('created').filter(function(it) { | ||
return it.get().value('name') !== 'gremlin'; | ||
g.v(id).out('knows').filter(function(it) { | ||
return it.get().value('age') !== 32; | ||
}); | ||
@@ -14,0 +14,0 @@ }; |
@@ -6,3 +6,3 @@ var gremlin = require('../'); | ||
var script = 'g.V[1..2]'; | ||
var script = 'g.V()[1..2]'; | ||
@@ -9,0 +9,0 @@ var s = client.stream(script); |
@@ -6,3 +6,3 @@ var gremlin = require('../'); | ||
var script = 'g.V[1..2]'; | ||
var script = 'g.V()[1..2]'; | ||
@@ -21,4 +21,4 @@ // Callback style | ||
s.on('end', function(message) { | ||
console.log("All results fetched", message); | ||
s.on('end', function() { | ||
console.log("All results fetched"); | ||
}); | ||
@@ -25,0 +25,0 @@ |
{ | ||
"name": "gremlin-client", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"description": "JavaScript client for TinkerPop3 Gremlin Server", | ||
@@ -28,3 +28,5 @@ "main": "index.js", | ||
"guid": "0.0.12", | ||
"highland": "^1.27.1", | ||
"lodash.defaults": "^2.4.1", | ||
"readable-stream": "^1.0.31", | ||
"ws": "^0.4.32" | ||
@@ -31,0 +33,0 @@ }, |
105
README.md
@@ -27,2 +27,5 @@ gremlin-client | ||
```javascript | ||
// Assuming Node.js or Browser environment with browserify: | ||
var gremlin = require('gremlin-client'); | ||
// Will open a WebSocket to ws://localhost:8182 by default | ||
@@ -41,3 +44,3 @@ var client = gremlin.createClient(); | ||
The `options` object currently allows you to set two options: | ||
The `options` object currently allows you to set the following options: | ||
* `session`: whether to use sessions or not (default: `false`) | ||
@@ -47,28 +50,38 @@ * `language`: the script engine to use on the server, see your gremlin-server.yaml file (default: `"gremlin-groovy"`) | ||
* `processor` (advanced usage): The name of the OpProcessor to utilize (default: `""`) | ||
* `accept`: (default: `"application/json"`) | ||
* `accept` (advanced usage): mime type of returned responses, depending on the serializer (default: `"application/json"`) | ||
### Sending scripts to Gremlin Server for execution | ||
The client supports two modes: streaming results, or traditional callback mode. | ||
The client supports three modes: | ||
* streaming results | ||
* callback mode (with internal buffer) | ||
* streaming protocol messages (for advanced usages) | ||
#### Stream mode: client.stream(script) | ||
#### Stream mode: client.stream(script, bindings, message) | ||
Return a Node.js stream which emits a `data` event with the raw data returned by Gremlin Server every time it receives a message (ie. the raw message returned by Gremlin Server). The stream simultaneously also emits a higher level `result` event, with `message.result` as a payload. | ||
Return a Node.js ReadableStream set in Object mode. The stream emits a `data` event for each result returned by Gremlin Server. | ||
The stream emits an `end` event when the client receives the last `statusCode: 299` message. | ||
For each low level protocol message with potentially one or more results attached (depending on the value of `resultIterationBatchSize` in your .yaml file), the stream will always reemit one `data` event per result. | ||
The order in which results are returned should be guaranteed, allowing you to effectively use `order` steps and the like in your Gremlin traversal. | ||
The stream emits an `end` event when the client receives the last `statusCode: 299` message returned by Gremlin Server. | ||
```javascript | ||
var query = client.stream('g.V()'); | ||
query.on('data', function(result, message) { | ||
// If playing with classic TinkerPop graph, will emit 6 data events | ||
query.on('data', function(result) { | ||
// Handle first vertex | ||
console.log(result); | ||
}); | ||
query.on('end', function(message) { | ||
console.log("All results fetched", message); | ||
query.on('end', function() { | ||
console.log("All results fetched"); | ||
}); | ||
``` | ||
#### Callback mode: client.execute(script, callback) | ||
This allows you to effectively `.pipe()` the stream to any other Node.js ReadableStream. | ||
#### Callback mode: client.execute(script, bindings, message, callback) | ||
Will execute the provided callback when all results are actually returned from the server. | ||
@@ -79,5 +92,5 @@ | ||
client.execute('g.V()', function(err, result, lastMessage, command) { | ||
client.execute('g.V()', function(err, results) { | ||
if (!err) { | ||
console.log(result) | ||
console.log(results) // Handle an array of results | ||
} | ||
@@ -87,9 +100,26 @@ }); | ||
The client will internally concatenate all partial results returned over different messages (possibly, depending on the total number of results and the value of `resultIterationBatchSize` set in your .yaml file). | ||
The client will internally concatenate all partial results returned over different messages (depending on the total number of results and the value of `resultIterationBatchSize` set in your .yaml file). | ||
When the client receives the final `statusCode: 299` message, the callback will be executed. | ||
#### Message stream mode: client.messageStream(script, bindings, message) | ||
A lower level method that returns a ReadableStream which emits the raw protocol messages returned by Gremlin Server as distinct `data` events. | ||
Although a public method, this is recommended for advanced usages only. | ||
```javascript | ||
var client = gremlin.createClient(); | ||
var query = client.messageStream('g.V()'); | ||
// Will emit 3 events with a resultIterationBatchSize set to 2 and classic graph | ||
query.on('data', function(message) { | ||
console.log(message.result); // Array of 2 vertices | ||
}); | ||
``` | ||
### Adding bound parameters to your scripts | ||
For better performance and security concerns, you may wish to send bound parameters to your scripts. | ||
For better performance and security concerns, you may wish to send bound parameters with your scripts. | ||
@@ -99,7 +129,9 @@ ```javascript | ||
client.execute('g.v(id)', { id: 1 }, function(err, result) { | ||
// Handle result | ||
client.execute('g.v(id)', { id: 1 }, function(err, results) { | ||
console.log(results[0]) // notice how results is always an array | ||
}); | ||
``` | ||
Also work with `client.stream()` and `client.messageStream()` for they share the same signature, without the callback as last parameter. | ||
### Overriding low level settings on a per request basis | ||
@@ -110,8 +142,9 @@ | ||
```javascript | ||
client.execute('g.v(1)', null, { args: { language: 'nashorn' }}, function(err, result) { | ||
client.execute('g.v(1)', null, { args: { language: 'nashorn' }}, function(err, results) { | ||
// Handle result | ||
}); | ||
``` | ||
Basically, all you have to do is provide an Object as third parameter to any `client.stream()`, `client.execute()` or `client.streamMessage()` methods. | ||
Because we're not sending any bound parameters, notice how the second argument **must** be set to `null` so the low level message object is not mistaken with bound arguments. | ||
Because we're not sending any bound parameters in this example, notice how the second argument **must** be set to `null` so the low level message object is not mistaken with bound arguments. | ||
@@ -121,3 +154,3 @@ If you wish to also send bound parameters while overriding the low level message, you can do the following: | ||
```javascript | ||
client.execute('g.v(id)', { id: 1 }, { args: { language: 'nashorn' }}, function(err, result) { | ||
client.execute('g.v(id)', { id: 1 }, { args: { language: 'nashorn' }}, function(err, results) { | ||
// Handle result | ||
@@ -127,12 +160,10 @@ }); | ||
The same method signature also applies to `client.stream()`: | ||
Or in stream mode: | ||
```javascript | ||
var s = client.stream(script, bindings, message); | ||
var s = client.stream('g.v(id)', { id: 1 }, { args: { language: 'nashorn' }}); | ||
``` | ||
### Using Gremlin-JavaScript syntax with Nashorn | ||
Providing your configured `nashorn` script engine in your `gremlin-server.yaml` file, you can send and execute Gremlin-JavaScript formatted queries: | ||
Providing your configured `nashorn` script engine in your `gremlin-server.yaml` file, you can send and execute Gremlin-JavaScript formatted queries (see example in this repository in `/config`): | ||
@@ -164,3 +195,3 @@ ```yaml | ||
// Send that script function body to Gremlin Server for execution in Nashorn engine | ||
client.execute(script, function(err, result) { | ||
client.execute(script, function(err, results) { | ||
// Handle result | ||
@@ -170,9 +201,11 @@ }); | ||
The client gets a string representation of the function passed to `client.stream()` or `client.query()` by calling the `.toString()` method. | ||
The client internally gets a string representation of the function passed to `client.stream()` or `client.execute()` by calling the `.toString()` method. | ||
Passing bound parameters and/or low level message will also work when using nashorn script engine. | ||
You may also simply pass a raw string as first parameter, rather than a function. The Function.toString() trick is just a convenient way to expose the full Groovy/Java API in your local JS environment. You can also use loop or try..catch that will be executed in the context of Gremlin Server. | ||
## Running the Examples | ||
This section assumes that you configured `resultIterationBatchSize: 1` in your Gremlin Server .yaml config file and loaded the default TinkerPop graph with `scripts: [scripts/generate-classic.groovy]` | ||
This section assumes that loaded the default TinkerPop graph with `scripts: [scripts/generate-classic.groovy]` in your .yaml config file. | ||
@@ -193,14 +226,10 @@ | ||
## Features | ||
* commands issued before the WebSocket is opened are queued and sent when it's ready. | ||
## To do list | ||
* handle any errors | ||
* reconnect WebSocket if connection is lost | ||
* support `.execute()` with promise | ||
* secure WebSocket | ||
* tests | ||
* better error handling | ||
* emit more client events | ||
* reconnect WebSocket if connection is lost? | ||
* support `.execute()` with promise? | ||
* add option for secure WebSocket | ||
* more tests | ||
* performance optimization | ||
@@ -6,3 +6,2 @@ /*jslint -W079 */ | ||
var inherits = require('util').inherits; | ||
var Stream = require('stream').Stream; | ||
@@ -14,3 +13,6 @@ var WebSocket = require('ws'); | ||
}; | ||
var highland = require('highland'); | ||
var MessageStream = require('./messagestream'); | ||
function GremlinClient(port, host, options) { | ||
@@ -56,3 +58,4 @@ this.port = port || 8182; | ||
/** | ||
* Process all incoming raw message events sent by Gremlin Server. | ||
* Process all incoming raw message events sent by Gremlin Server, and dispatch | ||
* to the appropriate command. | ||
* | ||
@@ -64,6 +67,7 @@ * @param {MessageEvent} event | ||
var command = this.commands[message.requestId]; | ||
var stream = command.stream; | ||
switch (message.code) { | ||
case 200: | ||
command.onData(message); | ||
stream.push(message); | ||
break; | ||
@@ -73,3 +77,3 @@ case 299: | ||
delete this.commands[message.requestId]; // TODO: optimize performance | ||
command.onEnd(message.result, message); | ||
stream.push(null); | ||
break; | ||
@@ -109,3 +113,3 @@ } | ||
command = this.queue.shift(); | ||
this.sendMessage(command); | ||
this.sendMessage(command.message); | ||
} | ||
@@ -129,3 +133,3 @@ }; | ||
command = commands[key]; | ||
command.terminate(error); | ||
command.stream.emit('error', error); | ||
}); | ||
@@ -138,11 +142,14 @@ }; | ||
* | ||
* @param {String} script | ||
* @param {String|Function} script | ||
* @param {Object} bindings | ||
* @param {Object} message | ||
* @param {Object} handlers | ||
*/ | ||
GremlinClient.prototype.buildCommand = function(script, bindings, message, handlers) { | ||
var guid = Guid.create().value; | ||
GremlinClient.prototype.buildCommand = function(script, bindings, message) { | ||
if (typeof script === 'function') { | ||
script = this.extractFunctionBody(script); | ||
} | ||
bindings = bindings || {}; | ||
var stream = new MessageStream({ objectMode: true }); | ||
var guid = Guid.create().value; | ||
var args = _.defaults(message && message.args || {}, { | ||
@@ -164,6 +171,3 @@ gremlin: script, | ||
message: message, | ||
onData: handlers.onData, | ||
onEnd: handlers.onEnd, | ||
terminate: handlers.terminate, | ||
result: [] | ||
stream: stream | ||
}; | ||
@@ -176,7 +180,9 @@ | ||
this.sendCommand(command); //todo improve for streams | ||
return command; | ||
}; | ||
GremlinClient.prototype.sendMessage = function(command) { | ||
this.ws.send(JSON.stringify(command.message)); | ||
GremlinClient.prototype.sendMessage = function(message) { | ||
this.ws.send(JSON.stringify(message)); | ||
}; | ||
@@ -197,14 +203,20 @@ | ||
/** | ||
* Asynchronously send a script to Gremlin Server for execution and fire | ||
* the provided callback when all results have been fetched. | ||
* | ||
* This method internally uses a stream to handle the potential concatenation | ||
* of results. | ||
* | ||
* Callback signature: (Error, Array<result>) | ||
* | ||
* @public | ||
* @param {String|Function} script | ||
* @param {Object} bindings | ||
* @param {Object} message | ||
* @param {Function} callback | ||
*/ | ||
GremlinClient.prototype.execute = function(script, bindings, message, callback) { | ||
if (typeof script === 'function') { | ||
script = this.extractFunctionBody(script); | ||
} | ||
callback = arguments[arguments.length - 1]; //todo: improve? | ||
// Signature: script, callback | ||
if (typeof bindings === 'function') { | ||
callback = bindings; | ||
bindings = {}; | ||
} | ||
// Signature: script, bindings, callback | ||
if (typeof message === 'function') { | ||
@@ -215,43 +227,72 @@ callback = message; | ||
var command = this.buildCommand(script, bindings, message, { | ||
onData: function(message) { | ||
this.result = this.result.concat(message.result); | ||
}, | ||
onEnd: function(result, message) { | ||
return callback(null, result, message, this); | ||
}, | ||
terminate: function(error) { | ||
return callback(error); | ||
} | ||
var stream = this.messageStream(script, bindings, message); | ||
var results = []; | ||
// Using a Highland.js stream here because it's not publicly exposed | ||
stream = highland(stream) | ||
.map(function(message) { return message.result; }); | ||
stream.on('data', function(data) { | ||
results = results.concat(data); | ||
}); | ||
this.sendCommand(command); | ||
stream.on('end', function() { | ||
callback(null, results); | ||
}); | ||
stream.on('error', function(error) { | ||
callback(new Error('Stream error: ' + error)); | ||
}); | ||
}; | ||
/** | ||
* Execute the script and return a stream of distinct/single results. | ||
* This method reemits a distinct data event for each returned result, which | ||
* makes the stream behave as if `resultIterationBatchSize` was set to 1. | ||
* | ||
* If you do not wish this behavior, please use client.messageStream() instead. | ||
* | ||
* Even though this method uses Highland.js internally, it does not return | ||
* a high level Highland readable stream so we do not risk having to deal | ||
* with unexpected API breaking changes as Highland.js evolves. | ||
* | ||
* @return {ReadableStream} A Node.js Stream2 | ||
*/ | ||
GremlinClient.prototype.stream = function(script, bindings, message) { | ||
if (typeof script === 'function') { | ||
script = this.extractFunctionBody(script); | ||
} | ||
var messageStream = this.messageStream(script, bindings, message); | ||
var _ = highland; // override lo-dash locally | ||
if (typeof message === 'function') { | ||
message = {}; | ||
} | ||
// Create a local highland 'through' pipeline so we don't expose | ||
// a Highland stream to the end user, but a standard Node.js Stream2 | ||
var through = _.pipeline( | ||
_.map(function(message) { | ||
return message.result; | ||
}), | ||
_.sequence() | ||
); | ||
var stream = new Stream(); | ||
var stream = messageStream.pipe(through); | ||
var command = this.buildCommand(script, bindings, message, { | ||
onData: function(data) { | ||
stream.emit('data', data.result, data); | ||
}, | ||
onEnd: function(result, message) { | ||
stream.emit('end', message); | ||
}, | ||
terminate: function(error) { | ||
stream.emit('error', error); | ||
} | ||
}); | ||
return stream; | ||
}; | ||
this.sendCommand(command); | ||
/** | ||
* Execute the script and return a stream of raw messages returned by Gremlin | ||
* Server. | ||
* This method does not reemit one distinct data event per result. It directly | ||
* emits the raw messages returned by Gremlin Server as they are received. | ||
* | ||
* Although public, this is a low level method intended to be used for | ||
* advanced usages. | ||
* | ||
* @public | ||
* @param {String|Function} script | ||
* @param {Object} bindings | ||
* @param {Object} message | ||
* @return {MessageStream} | ||
*/ | ||
GremlinClient.prototype.messageStream = function(script, bindings, message) { | ||
var command = this.buildCommand(script, bindings, message); | ||
return stream; | ||
return command.stream; | ||
}; | ||
@@ -269,3 +310,3 @@ | ||
if (this.connected) { | ||
this.sendMessage(command); | ||
this.sendMessage(command.message); | ||
} else { | ||
@@ -272,0 +313,0 @@ this.queue.push(command); |
@@ -60,3 +60,3 @@ var gremlin = require('../'); | ||
s.on('data', function(result) { | ||
result.length.should.equal(1); | ||
result.should.be.an('object'); | ||
}); | ||
@@ -63,0 +63,0 @@ |
@@ -8,7 +8,5 @@ /*jshint -W030 */ | ||
client.execute('g.V()', function(err, result, response, command) { | ||
client.execute('g.V()', function(err, result) { | ||
(err === null).should.be.true; | ||
result.length.should.equal(6); | ||
response.should.exist; | ||
response.code.should.equal(299); | ||
done(); | ||
@@ -15,0 +13,0 @@ }); |
@@ -7,16 +7,13 @@ var gremlin = require('../'); | ||
var client = gremlin.createClient({ language: 'nashorn' }); | ||
var count = 0; | ||
var s = client.stream(function() { g.V(); }); | ||
s.on('data', function(result, response) { | ||
result.should.exist; | ||
response.should.exist; | ||
response.code.should.equal(200); | ||
var results = []; | ||
count += 1; | ||
s.on('data', function(result) { | ||
results.push(result); | ||
}); | ||
s.on('end', function() { | ||
count.should.equal(6); | ||
results.length.should.equal(6); | ||
done(); | ||
@@ -31,5 +28,4 @@ }); | ||
s.on('data', function(result, response) { | ||
result.length.should.equal(1); | ||
response.code.should.equal(200); | ||
s.on('data', function(result) { | ||
result.should.be.an('object'); | ||
}); | ||
@@ -47,5 +43,4 @@ | ||
s.on('data', function(result, response) { | ||
result.length.should.equal(1); | ||
response.code.should.equal(200); | ||
s.on('data', function(result) { | ||
result.should.be.an('object'); | ||
}); | ||
@@ -63,5 +58,4 @@ | ||
s.on('data', function(result, response) { | ||
result.length.should.equal(1); | ||
response.code.should.equal(200); | ||
s.on('data', function(result) { | ||
result.should.be.an('object'); | ||
}); | ||
@@ -68,0 +62,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
861889
24
11383
224
5
+ Addedhighland@^1.27.1
+ Addedreadable-stream@^1.0.31
+ Addedcore-util-is@1.0.3(transitive)
+ Addedhighland@1.29.0(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.1(transitive)
+ Addedreadable-stream@1.1.14(transitive)
+ Addedstring_decoder@0.10.31(transitive)