orchestrate
Version:
Orchestrate is a database service. It is a simple REST API that is optimized for queries. Orchestrate combines full-text search, graph, time-series, and key/value.
276 lines (231 loc) • 7.85 kB
JavaScript
// Copyright 2013 Bowery Software, LLC
/**
* @fileoverview Graph relation builder.
*/
// Module Dependencies.
var assert = require('assert')
var util = require('util')
var Builder = require('./builder')
/**
* @constructor
*/
function GraphBuilder () {}
util.inherits(GraphBuilder, Builder)
/**
* Get a relationship.
* @return {GraphBuilder}
*/
GraphBuilder.prototype.get = function () {
this._method = 'get'
return this
}
/**
* Create new relationship.
* @return {GraphBuilder}
*/
GraphBuilder.prototype.create = function () {
this._method = 'put'
return this
}
/**
* 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.
* @return {GraphBuilder}
*/
GraphBuilder.prototype.remove = function () {
this._method = 'del'
return this
}
/**
* Set graph origin.
* @param {string} collection
* @param {string} key
* @return {GraphBuilder}
*/
GraphBuilder.prototype.from = function (collection, key) {
assert(collection && key, 'Collection and key required.')
this.collection = collection
this.key = key
return this
}
/**
* Enable field-filtering, with a whitelist of field names
* @example
* // adds a whitelist field-filter for the field value.age
* graphBuilder.withFields('value.age');
* @example
* // adds two whitelist field-filters, for the fields value.name and value.age
* graphBuilder.withFields('value.name', 'value.age');
* @param {...string} fieldNames Fully-qualified field names to use as whitelist field-filters
* @return {GraphBuilder}
*/
GraphBuilder.prototype.withFields = function () {
this.filterWithFields = this.filterWithFields || [];
this.filterWithFields = this.filterWithFields.concat(Array.prototype.slice.call(arguments));
return this;
}
/**
* Enable field-filtering, with a blacklist of field names
* @example
* // adds a blacklist field-filter for the field value.age
* graphBuilder.withoutFields('value.age');
* @example
* // adds two blacklist field-filters, for the fields value.name and value.age
* graphBuilder.withoutFields('value.name', 'value.age');
* @param {...string} fieldNames Fully-qualified field names to use as blacklist field-filters
* @return {GraphBuilder}
*/
GraphBuilder.prototype.withoutFields = function () {
this.filterWithoutFields = this.filterWithoutFields || [];
this.filterWithoutFields = this.filterWithoutFields.concat(Array.prototype.slice.call(arguments));
return this;
}
/**
* Set graph relation.
* @param {string} kind
* @return {GraphBuilder}
*/
GraphBuilder.prototype.related = function (kind) {
assert(kind, 'Kind of relation required.')
// Hoist the kind argument into an array.
if (util.isArray(kind)) {
this.kind = kind;
} else {
this.kind = Array.prototype.slice.call(arguments, 0)
}
// 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
}
/**
* Set graph destination.
* @param {string} toCollection
* @param {string} toKey
* @return {Object}
*/
GraphBuilder.prototype.to = function (toCollection, toKey) {
assert(toCollection && toKey, 'toCollection and toKey required.')
this.toCollection = toCollection
this.toKey = toKey
// 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
}
/**
* Set graph result limit.
* @param {Number} limit
* @return {Object}
*/
GraphBuilder.prototype.limit = function (limit) {
assert(limit || limit == 0, 'Limit required.')
this.limit_value = limit
return this
}
/**
* Set graph result offset.
* @param {Number} offset
* @return {Object}
*/
GraphBuilder.prototype.offset = function (offset) {
assert.equal(typeof offset, 'number', 'Offset required.')
this.offset_value = offset
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.
* @param {string} method
* @return {Object}
* @protected
*/
GraphBuilder.prototype._execute = function (method) {
// 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)
// 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)
// 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 = {}
if (this._method == 'del') query['purge'] = true
if (this.limit_value) query['limit'] = this.limit_value
if (this.offset_value) query['offset'] = this.offset_value
if (this.filterWithFields) query['with_fields'] = this.filterWithFields
if (this.filterWithoutFields) query['without_fields'] = this.filterWithoutFields
// 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)
}
// Module Exports.
module.exports = GraphBuilder