orchestrate
Advanced tools
Comparing version 0.4.6 to 0.4.7
@@ -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 [![Build Status](https://travis-ci.org/orchestrate-io/orchestrate.js.png)](https://travis-ci.org/orchestrate-io/orchestrate.js) [![Coverage Status](https://coveralls.io/repos/orchestrate-io/orchestrate.js/badge.png)](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
105324
2538
568