orchestrate
Advanced tools
Comparing version
@@ -136,3 +136,3 @@ // Copyright 2013 Bowery Software, LLC | ||
* @param {Object} data | ||
* @param {string|boolean} | ||
* @param {string|boolean} match | ||
* @return {Promise} | ||
@@ -139,0 +139,0 @@ */ |
119
lib/graph.js
@@ -9,2 +9,3 @@ // Copyright 2013 Bowery Software, LLC | ||
var assert = require('assert') | ||
var util = require('util') | ||
var Builder = require('./builder') | ||
@@ -19,3 +20,3 @@ | ||
require('util').inherits(GraphBuilder, Builder) | ||
util.inherits(GraphBuilder, Builder) | ||
@@ -44,2 +45,27 @@ | ||
/** | ||
* Set graph data. | ||
* @param {Object} data | ||
* @return {GraphBuilder} | ||
*/ | ||
GraphBuilder.prototype.data = function (data) { | ||
assert(data, 'Data required.') | ||
this.data = data | ||
return this | ||
} | ||
/** | ||
* Set "If-Match" header to the given ref value. | ||
* @param {String|boolean} ref. String ref for conditional update, or false | ||
* for insert-if-absent (ie fail create if already present) | ||
* @return {GraphBuilder} | ||
*/ | ||
GraphBuilder.prototype.ref = function (ref) { | ||
assert(typeof ref === 'string' || typeof ref === 'boolean', 'Ref required.') | ||
this.ref = ref | ||
return this | ||
} | ||
/** | ||
* Delete a relationship. | ||
@@ -76,5 +102,28 @@ * @return {GraphBuilder} | ||
this.kind = Array.prototype.slice.call(arguments, 0) | ||
// Hoist the kind argument into an array. | ||
if (util.isArray(kind)) { | ||
this.kind = kind; | ||
} else { | ||
this.kind = Array.prototype.slice.call(arguments, 0) | ||
} | ||
if (!this.write) return this._execute(this._method) | ||
// Make sure that the kind array is non-empty, and that its elements are non-empty strings. | ||
assert(this.kind.length > 0, 'Kind of relation required.') | ||
for (var i = 0; i < this.kind.length; i++) { | ||
var k = this.kind[i]; | ||
assert(typeof(k) === "string" && k.length > 0, 'Kind must be a non-empty string') | ||
} | ||
// Call the execute method in any of these scenarios: | ||
// (1) This is a read-only GraphBuilder, with toCollection & toKey empty. The execute | ||
// function will list (GET) all related items for the collection & key. | ||
// (2) This is a read-only GraphBuilder, with toCollection & toKey non-empty. The | ||
// execute function will retrieve (GET) the data of a specific relationship. | ||
// (3) This is a write-only GraphBuilder, with toCollection & toKey non-empty. The | ||
// execute function will submit (PUT) a new relationship. | ||
if (!this.write || (this.toCollection && this.toKey)) { | ||
return this._execute(this._method) | ||
} | ||
// Without toCollection and toKey on this write-only GraphBuilder, just return | ||
// the builder, since there are still missing params. | ||
return this | ||
@@ -94,3 +143,9 @@ } | ||
this.toKey = toKey | ||
return this._execute(this._method) | ||
// Call the execute method only if the relationship kind has already been set. | ||
// Otherwise, just return the builder, since there ae still missing params. | ||
if (this.kind) { | ||
return this._execute(this._method) | ||
} | ||
return this | ||
} | ||
@@ -105,3 +160,3 @@ | ||
GraphBuilder.prototype.limit = function (limit) { | ||
assert(limit, 'Limit required.') | ||
assert(limit || limit == 0, 'Limit required.') | ||
this.limit_value = limit | ||
@@ -125,2 +180,13 @@ return this | ||
/** | ||
* Quote the provided string if not already quoted. | ||
* @param {string} str | ||
* @return {string} | ||
* @protected | ||
*/ | ||
GraphBuilder.prototype._quote = function (str) { | ||
return str.charAt(0) == '"' ? str : '"' + str + '"' | ||
} | ||
/** | ||
* Execute graph read/write. | ||
@@ -132,14 +198,30 @@ * @param {string} method | ||
GraphBuilder.prototype._execute = function (method) { | ||
var relation = this.write ? 'relation' : 'relations' | ||
// Make sure we have a from item key | ||
assert(this.collection && this.key, "'from' collection and key required.") | ||
// Make sure that the kind array is non-empty, and that its elements are non-empty strings. | ||
assert(this.kind && this.kind.length > 0, 'Kind of relation required.') | ||
for (var i = 0; i < this.kind.length; i++) { | ||
var k = this.kind[i]; | ||
assert(typeof(k) === "string" && k.length > 0, 'Kind must be a non-empty string') | ||
} | ||
// Create an array of path components. | ||
var pathArgs = [] | ||
pathArgs.push(this.collection) | ||
pathArgs.push(this.key) | ||
pathArgs.push(relation) | ||
// The 'relation' path component is used for creating and retrieving individual relations. | ||
// The 'relations' path component is used for traversing single-hop or multi-hop relationships. | ||
if (this.write || (this._method === "GET" && this.toCollection && this.toKey)) { | ||
pathArgs.push('relation') | ||
} else { | ||
pathArgs.push('relations') | ||
} | ||
pathArgs = pathArgs.concat(this.kind) | ||
pathArgs.push(this.toCollection) | ||
pathArgs.push(this.toKey) | ||
pathArgs = pathArgs.filter(function (i) { | ||
return i != undefined | ||
}) | ||
// Destination collection and key are only mandatory during PUT and (non-listing) GET. | ||
if (this.toCollection) pathArgs.push(this.toCollection) | ||
if (this.toKey) pathArgs.push(this.toKey) | ||
// Build the querystring | ||
var query = {} | ||
@@ -150,4 +232,13 @@ if (this._method == 'del') query['purge'] = true | ||
var url = this.getDelegate() && this.getDelegate().generateApiUrl(pathArgs, query) | ||
return this.getDelegate()['_' + this._method](url) | ||
// Build headers | ||
var header = {} | ||
if (typeof(this.ref) === 'string') { | ||
header['If-Match'] = this._quote(this.ref) | ||
} else if (typeof(this.ref) === 'boolean' && this.ref === false) { | ||
header['If-None-Match'] = '"*"' | ||
} | ||
// Build the URL and return a callable delegate for this request | ||
var url = this.getDelegate() && this.getDelegate().generateApiUrl(pathArgs, query) | ||
return this.getDelegate()['_' + this._method](url, this.data, header) | ||
} | ||
@@ -154,0 +245,0 @@ |
@@ -28,3 +28,3 @@ { | ||
}, | ||
"version": "0.4.6", | ||
"version": "0.4.7", | ||
"main": "index", | ||
@@ -31,0 +31,0 @@ "tags": [ |
@@ -383,3 +383,49 @@ # orchestrate.js [](https://travis-ci.org/orchestrate-io/orchestrate.js) [](https://coveralls.io/r/orchestrate-io/orchestrate.js) | ||
We can optionally include a JSON object representing the properties of the relationship -- which are distinct from the properties of the two items connected by the relationship -- like this: | ||
```javascript | ||
db.newGraphBuilder() | ||
.create() | ||
.data({ "rating" : "5 Stars" }) | ||
.from('users', 'Steve') | ||
.related('likes') | ||
.to('movies', 'Superbad') | ||
``` | ||
We can use [conditional put statements](https://orchestrate.io/docs/api/#key/value/put-(create/update)) to determine whether or not the store operation will occur. If a ref value is provided an `update` will occur if there is a valid match, if false is provided, a `create` will occur if there is no match. | ||
```javascript | ||
// update if ref matches | ||
db.newGraphBuilder() | ||
.create() | ||
.data({ "rating" : "4 Stars" }) | ||
.ref('cbb48f9464612f20') | ||
.from('users', 'Steve') | ||
.related('likes') | ||
.to('movies', 'Superbad') | ||
// create if no previous relationship | ||
db.newGraphBuilder() | ||
.create() | ||
.data({ "rating" : "4 Stars" }) | ||
.ref(false) | ||
.from('users', 'Steve') | ||
.related('likes') | ||
.to('movies', 'Superbad') | ||
``` | ||
After storing this relationship, we can retrieve its properties like this: | ||
```javascript | ||
db.newGraphReader() | ||
.get() | ||
.from('users', 'Steve') | ||
.related('likes') | ||
.to('movies', 'Superbad') | ||
``` | ||
We can then look up all the different items Steve likes: | ||
```javascript | ||
@@ -393,2 +439,3 @@ db.newGraphReader() | ||
We can even take this another step further: | ||
```javascript | ||
@@ -400,5 +447,7 @@ db.newGraphReader() | ||
``` | ||
This will return all of the things that friends of Steve have liked. This assumes a friend relation has previously been defined between Steve and another user. | ||
Orchestrate supports offsets and limits for graph relationships as well. To set those values: | ||
```javascript | ||
@@ -414,2 +463,3 @@ db.newGraphReader() | ||
If we want to delete a graph relationship: | ||
```javascript | ||
@@ -427,2 +477,3 @@ db.newGraphBuilder() | ||
Creating an event: | ||
```javascript | ||
@@ -437,2 +488,3 @@ db.newEventBuilder() | ||
Creating an event at a specified time: | ||
```javascript | ||
@@ -448,2 +500,3 @@ db.newEventBuilder() | ||
Listing events: | ||
```javascript | ||
@@ -450,0 +503,0 @@ db.newEventReader() |
@@ -13,10 +13,28 @@ // Copyright 2014 Orchestrate, Inc. | ||
var r = function(collection, from, to, kind) { | ||
return db.newGraphBuilder() | ||
.create() | ||
var createRelation = function(collection, from, to, kind, data, ref) { | ||
var builder = db.newGraphBuilder().create(); | ||
if (data) builder.data(data); | ||
if (ref) builder.ref(ref); | ||
return builder.from(collection, from) | ||
.to(collection, to) | ||
.related(kind); | ||
}; | ||
var getRelation = function(collection, from, to, kind) { | ||
return db.newGraphReader() | ||
.get() | ||
.from(collection, from) | ||
.related(kind) | ||
.to(collection, to); | ||
.to(collection, to) | ||
.related(kind); | ||
}; | ||
var listRelations = function(collection, from, kinds) { | ||
return db.newGraphReader() | ||
.get() | ||
.from(collection, from) | ||
.related(kinds); | ||
}; | ||
suite('Graph', function () { | ||
@@ -34,4 +52,6 @@ suiteSetup(function (done) { | ||
test('Create graph relationships', function(done) { | ||
var relations = [r(users.collection, users.steve.email, users.kelsey.email, "friend"), | ||
r(users.collection, users.kelsey.email, users.david.email, "friend")]; | ||
var relations = [ | ||
createRelation(users.collection, users.steve.email, users.kelsey.email, "friend"), | ||
createRelation(users.collection, users.kelsey.email, users.david.email, "friend") | ||
] | ||
@@ -51,7 +71,90 @@ Q.all(relations) | ||
test('Create graph relationship with properties', function(done) { | ||
var properties = [ | ||
{ "foo" : "bar" }, | ||
{ "bing" : "bong" } | ||
]; | ||
var relations = [ | ||
createRelation(users.collection, users.steve.email, users.kelsey.email, "likes", properties[0]), | ||
createRelation(users.collection, users.kelsey.email, users.david.email, "likes", properties[1]) | ||
]; | ||
Q.all(relations) | ||
.then(function (res) { | ||
assert.equal(2, res.length); | ||
// Test that each of the requests succeeded | ||
for (var i in res) { | ||
assert.equal(201, res[i].statusCode); | ||
} | ||
// Retrieve each of the relations and make sure they contain the correct properties | ||
checkRelationProperties(users.collection, users.steve.email, users.kelsey.email, "likes", properties[0], done); | ||
checkRelationProperties(users.collection, users.kelsey.email, users.david.email, "likes", properties[1], done); | ||
done(); | ||
}) | ||
.fail(function (res) { | ||
done(res); | ||
}); | ||
}); | ||
test('Conditionally create (if-match) graph relationship with properties', function(done) { | ||
var kind = "coworkers" | ||
var properties = [ | ||
{ "foo" : "bar" }, | ||
{ "fizz" : "buzz" }, | ||
{ "bing" : "bong" } | ||
]; | ||
var properties2 = [ | ||
{ "foo2" : "bar2" }, | ||
{ "fizz2" : "buzz2" }, | ||
{ "bing2" : "bong2" } | ||
]; | ||
// For starters, only create the relationship if it doesn't already exist | ||
var relations = [ | ||
createRelation(users.collection, users.steve.email, users.david.email, kind, properties[0], false), | ||
createRelation(users.collection, users.steve.email, users.kelsey.email, kind, properties[1], false), | ||
createRelation(users.collection, users.kelsey.email, users.david.email, kind, properties[2], false) | ||
]; | ||
Q.all(relations) | ||
.then(function (res) { | ||
assert.equal(3, res.length); | ||
// Test that each of the requests succeeded, and retrieve their etags. | ||
var etags = []; | ||
for (var i in res) { | ||
assert.equal(201, res[i].statusCode); | ||
etags.push(res[i].etag); | ||
} | ||
// Update both relationships. | ||
// The first one, using if-none-match again, even though the item already exists, should fail. | ||
// The second one, using if-match with a bogus etag, should also fail. | ||
// The third one, using its previous etag to implement if-match, should succeed. | ||
var updates = [ | ||
createRelation(users.collection, users.steve.email, users.david.email, kind, properties2[0], false), | ||
createRelation(users.collection, users.steve.email, users.kelsey.email, kind, properties2[1], '"nonsense"'), | ||
createRelation(users.collection, users.kelsey.email, users.david.email, kind, properties2[2], etags[2]) | ||
]; | ||
// Process the updates | ||
Q.all(updates) | ||
.fin(function() { | ||
// Retrieve each of the relations and make sure they contain the correct properties. | ||
// Only the third one should have been updated, but the other two should contain the original json. | ||
checkRelationProperties(users.collection, users.steve.email, users.david.email, kind, properties[0]), | ||
checkRelationProperties(users.collection, users.steve.email, users.kelsey.email, kind, properties[1]), | ||
checkRelationProperties(users.collection, users.kelsey.email, users.david.email, kind, properties2[2]) | ||
done(); | ||
}); | ||
}) | ||
.fail(function (res) { | ||
done(res); | ||
}); | ||
}); | ||
test('Traverse graph relationship', function(done) { | ||
db.newGraphReader() | ||
.get() | ||
.from(users.collection, users.steve.email) | ||
.related('friend', 'friend') | ||
listRelations(users.collection, users.steve.email, [ 'friend', 'friend' ]) | ||
.then(function (res) { | ||
@@ -90,2 +193,14 @@ assert.equal(200, res.statusCode); | ||
function checkRelationProperties(collection, key, toCollection, toKey, kind, properties, done) { | ||
var promise = getRelation(collection, key, toCollection, toKey, kind); | ||
Q.all(promise) | ||
.then(function(res) { | ||
assert.equal(res.body, properties); | ||
done(); | ||
}) | ||
.fail(function (res) { | ||
done(res); | ||
}); | ||
} | ||
}); |
@@ -12,2 +12,3 @@ // Copyright 2014 Orchestrate, Inc. | ||
var util = require('util'); | ||
var misc = require('./misc'); | ||
@@ -71,3 +72,3 @@ suite('Key-Value', function () { | ||
assert.deepEqual(users.david, res.body.results[0].value); | ||
assert.equal('/v0/'+users.collection+'?limit=1&afterKey=byrd@bowery.io', res.body.next); | ||
misc.assertUrlsEqual(res.body.next, '/v0/'+users.collection+'?limit=1&afterKey=byrd@bowery.io'); | ||
return db.list(users.collection, {limit:1, afterKey:users.david.email}); | ||
@@ -74,0 +75,0 @@ }) |
@@ -11,3 +11,20 @@ // Copyright 2014 Orchestrate, Inc. | ||
var util = require('util'); | ||
var url = require('url'); | ||
function assertUrlsEqual(a, b) { | ||
var aUrl = url.parse(a); | ||
var bUrl = url.parse(b); | ||
assert.equal(aUrl.hostname, bUrl.hostname); | ||
assert.equal(aUrl.port, bUrl.port); | ||
assert.equal(aUrl.hash, bUrl.hash); | ||
var aUrlQueryParts = aUrl.query.split("&"); | ||
var bUrlQueryParts = bUrl.query.split("&"); | ||
assert.equal(aUrlQueryParts.length, bUrlQueryParts.length); | ||
aUrlQueryParts.sort(); | ||
bUrlQueryParts.sort(); | ||
for (var i = 0; i < aUrlQueryParts.length; i++) { | ||
assert.equal(aUrlQueryParts[i], bUrlQueryParts[i]); | ||
} | ||
} | ||
suite('Misc', function () { | ||
@@ -27,1 +44,3 @@ test('Service ping', function(done) { | ||
}); | ||
module.exports.assertUrlsEqual = assertUrlsEqual; |
@@ -12,3 +12,3 @@ // Copyright 2014 Orchestrate, Inc. | ||
var collection = 'patch.test' | ||
var collection = 'patch.test_' + process.version; | ||
var key = 'test-key-1' | ||
@@ -15,0 +15,0 @@ // example doc used in all tests. it is reset every before each test. |
@@ -12,2 +12,3 @@ // Copyright 2014 Orchestrate, Inc. | ||
var util = require('util'); | ||
var misc = require('./misc'); | ||
@@ -70,3 +71,3 @@ suite('Search', function () { | ||
assert.equal(1, res.body.count); | ||
assert.equal(res.body.next, '/v0/'+users.collection+'?limit=1&query=*&offset=2'); | ||
misc.assertUrlsEqual(res.body.next, '/v0/'+users.collection+'?limit=1&query=*&offset=2'); | ||
done(); | ||
@@ -73,0 +74,0 @@ }) |
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
105324
10.02%2538
8.46%568
10.29%