Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit 9a45311feb94e5e54b43c953ff00889f3641b052 Francisco Treacy committed Mar 3, 2010
Showing with 462 additions and 0 deletions.
  1. +54 −0 README.md
  2. 0 riak-jquery.js
  3. +110 −0 riak-node.js
  4. +126 −0 riak.js
  5. +113 −0 test/airport-test.js
  6. +59 −0 test/riak-test.js
54 README.md
@@ -0,0 +1,54 @@
+## riak-js
+
+### A Javascript library for Riak
+
+#### Features so far
+
+ - Sensible but always overridable defaults
+ - Operations: get (bucket or document), save, remove, walk, mapReduce
+ - Currently only available for node.js v0.1.30+ - soon a browser/jQuery implementation
+ - Tested with latest Riak tip (932:a98ed16a9d87)
+
+#### Defaults
+
+All operations take an _options_ object as the last argument. These specified options will override the defaults, which are defined as:
+
+ {
+ method: 'GET',
+ interface: 'riak',
+ headers: {},
+ callback: function(response, meta) { Riak.prototype.log(response) },
+ errback: function(response, meta) { Riak.prototype.log(meta.statusCode + ": " + response, 'error') },
+ returnbody: false
+ }
+
+as well as `localhost` for the host and `8098` for the port.
+
+#### Noteworthy items
+
+ - All operations return a function that takes two arguments (two functions: callback and errback). Therefore you *must* call it for something to happen: `db.get('bucket')()`
+ - These functions are passed in two arguments, the `response` and a `meta` object
+ - Headers are exposed through `meta.headers` and the status code through `meta.statusCode`
+ - All operations accept an `options` object which will be *mixed-in* with the defaults
+ - If no `Content-Type` is provided in the headers, `application/json` will be assumed; which in turn will be serialized into JSON
+ - Link-walking is done through the map/reduce facility (easier to handle responses)
+ - If no `language` is provided in any map/reduce phase, `language: javascript` is assumed
+
+#### An example session for node.js would be:
+
+ require.paths.unshift(".");
+ var Riak = require('riak-node'), db = new Riak.Client();
+
+Get and save a document
+
+ db.get('albums', 4)(function(response, meta) {
+ response.tracks = 12;
+ db.save(response)(); // here we use the provided default callbacks that log the result
+ }, function(error_response, meta) {
+ // something in case of error
+ });
+
+Check out the `airport-test.js` file for more.
+
+// http.Client queues all requests, so if you want to run requests in parallel
+// you need to create one client instance for each request
0 riak-jquery.js
No changes.
110 riak-node.js
@@ -0,0 +1,110 @@
+// This file is provided to you under the Apache License,
+// Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+var sys = require('sys'),
+ http = require('http'),
+ querystring = require('querystring');
+
+var Riak = exports.Client = require('./riak').common;
+
+Riak.prototype.getClient = function(port, host) {
+ return http.createClient(port, host);
+}
+
+Riak.prototype.log = function(message, error) {
+ sys.puts('[riak-js] ' + (error ? 'ERROR: ' : '') + message);
+}
+
+Riak.prototype.stringifyQuery = function(query) {
+ return querystring.stringify(query);
+}
+
+Riak.prototype.mixin = function(target, obj1, obj2) {
+ return process.mixin(true, {}, obj1, obj2);
+}
+
+Riak.prototype.execute = function (url, options) {
+
+ var self = this;
+
+ return function(callback, errback) {
+
+ options = process.mixin({}, self.defaults, options);
+
+ callback = callback || options.callback;
+ errback = errback || options.errback;
+
+ if (options.headers['content-type'] === undefined) {
+ options.headers['content-type'] = 'application/json';
+ }
+
+ if (options.headers['content-type'] === 'application/json') {
+ try {
+ options.data = self.toJSON(options.data);
+ } catch (e) {} // no-op if error
+ }
+
+ var queryProperties = {};
+
+ // known url params are added to the query
+ ['r', 'w', 'dw', 'rw', 'nocache', 'returnbody', 'keys'].forEach(function(p) {
+ if (options[p]) {
+ queryProperties[p] = options[p];
+ }
+ });
+
+ var query = self.toQuery(queryProperties);
+
+ var path = '/' + options.interface + '/' + url + (query ? ('?' + query) : ''),
+ request = self.client.request(options.method.toUpperCase(), path, options.headers);
+
+ self.log(options.method.toUpperCase() + ' ' + path);
+ // sys.debug(sys.inspect(options.headers));
+
+ if (options.data) {
+ request.write(options.data, options.requestEncoding || 'utf8');
+ }
+
+ request.addListener('response', function(response) {
+
+ var buffer = "", meta = {};
+ meta.headers = response.headers;
+ meta.statusCode = status = response.statusCode;
+
+ response.addListener('data', function(chunk) {
+ buffer += chunk;
+ });
+
+ response.addListener('end', function() {
+
+ if (status >= 400) {
+ errback(buffer, meta);
+ } else {
+ if (status !== 204 && buffer !== '' && meta.headers['content-type'] === 'application/json') {
+ try {
+ callback(JSON.parse(buffer), meta);
+ } catch (e) {
+ errback(buffer, meta, "Couldn't parse it as JSON");
+ }
+ } else {
+ callback(buffer, meta);
+ }
+ }
+
+ })
+
+ });
+ request.close();
+ }
+}
126 riak.js
@@ -0,0 +1,126 @@
+// This file is provided to you under the Apache License,
+// Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+var Riak = function (port, host, settings) {
+ port = port || 8098;
+ host = host || 'localhost';
+
+ this.defaults = Riak.prototype.mixin({}, this.defaults, settings);
+ this.client = Riak.prototype.getClient(port, host);
+}
+
+Riak.prototype.defaults = {
+ method: 'GET',
+ interface: 'riak',
+ headers: {},
+ callback: function(response, meta) { Riak.prototype.log(response) },
+ errback: function(response, meta) { Riak.prototype.log(meta.statusCode + ": " + response, 'error') },
+ returnbody: false
+}
+
+// db operations
+
+Riak.prototype.get = function(bucket, key, options) {
+ return this.execute(this.path(bucket, key), options);
+}
+
+Riak.prototype.remove = function(bucket, key, options) {
+ options = this.ensure(options);
+ options.method = 'DELETE';
+ return this.execute(this.path(bucket, key), options);
+}
+
+Riak.prototype.save = function(bucket, key, data, options) {
+ data = this.ensure(data);
+ options = this.ensure(options);
+ options.method = key ? 'PUT' : 'POST';
+ options.data = data;
+
+ return this.execute(this.path(bucket, key), options);
+}
+
+Riak.prototype.walk = function(bucket, key, spec, options) {
+
+ var query = spec.map(function(unit) {
+ return { link: { bucket: unit[0] || "_", tag: unit[1] || "_", keep: unit[2] ? true : false } };
+ }),
+ reduce = { reduce :{ language :"erlang", module: "riak_mapreduce", "function" :"reduce_set_union"}},
+ map = { map: { name: "Riak.mapValuesJson"}};
+
+ query.push(reduce, map);
+
+ return this.mapReduce({ inputs: [[bucket, key]], "query": query }, options);
+}
+
+Riak.prototype.mapReduce = function(query, options) {
+ options = this.ensure(options);
+ options.interface = 'mapred';
+ options.method = 'POST';
+ options.headers = { 'content-type': 'application/json' };
+
+ query.query.forEach(function(phase) {
+ for (p in phase) { // map, reduce or link
+ if (phase[p].language === undefined) {
+ phase[p].language = 'javascript';
+ };
+ }
+ })
+
+ options.data = query;
+
+ return this.execute('', options);
+}
+
+// utils
+
+Riak.prototype.path = function(bucket, key) {
+ return bucket + '/' + (key ? key : '');
+}
+
+Riak.prototype.makeLinks = function(links) {
+ var i = this.defaults.interface;
+ return links.map(function(link) {
+ link.tag = link.tag || "_";
+ return '</' + i + '/' + link.bucket + '/' + link.key + '>; riaktag="' + link.tag + '"';
+ }).join(", ");
+}
+
+Riak.prototype.toQuery = function(query) {
+ // use boolean strings since riak expects those
+ for (var k in query) {
+ if (typeof query[k] == 'boolean') {
+ query[k] = String(query[k]);
+ }
+ }
+ return this.stringifyQuery(query);
+}
+
+Riak.prototype.toJSON = function(data) {
+ return JSON.stringify(data, function(key, val) {
+ if (typeof val == 'function') {
+ return val.toString();
+ }
+ return val;
+ });
+}
+
+Riak.prototype.ensure = function(obj) {
+ return obj || {};
+}
+
+// exports
+
+if (exports) {
+ exports.common = Riak;
+}
113 test/airport-test.js
@@ -0,0 +1,113 @@
+// This file is provided to you under the Apache License,
+// Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+var sys = require('sys'),
+ Riak = require('../riak-node'),
+ assert = require('assert');
+
+var db = new Riak.Client(),
+ airline_bucket = 'test-airlines',
+ airport_bucket = 'test-airports',
+ flight_bucket = 'test-flights';
+
+// setup
+
+db.save(airport_bucket, 'EZE', {city: 'Buenos Aires'})();
+db.save(airport_bucket, 'BCN', {city: 'Barcelona'})();
+db.save(airport_bucket, 'AMS', {city: 'Amsterdam'})();
+db.save(airport_bucket, 'CDG', {city: 'Paris'})();
+db.save(airport_bucket, 'MUC', {city: 'Munich'})();
+db.save(airport_bucket, 'JFK', {city: 'New York'})();
+db.save(airport_bucket, 'HKK', {city: 'Hong Kong'})();
+db.save(airport_bucket, 'MEX', {city: 'Mexico DF'})();
+
+db.save(flight_bucket, 'KLM-8098', {code: 'KLM-8098', to: 'JFK', from: 'AMS', departure: 'Mon, 05 Jul 2010 17:05:00 GMT'})();
+db.save(flight_bucket, 'AFR-394', {code: 'AFR-394', to: 'CDG', from: 'EZE', departure: 'Mon, 12 Jul 2010 05:35:00 GMT'})();
+db.save(flight_bucket, 'CPA-112', {code: 'CPA-112', to: 'HKK', from: 'AMS', departure: 'Wed, 11 Aug 2010 01:20:00 GMT'})();
+db.save(flight_bucket, 'IBE-5624', {code: 'IBE-5624', to: 'MUC', from: 'BCN', departure: 'Mon, 15 Mar 2010 22:10:00 GMT'})();
+db.save(flight_bucket, 'ARG-714', {code: 'ARG-714', to: 'EZE', from: 'BCN', departure: 'Mon, 08 Mar 2010 20:50:00 GMT'})();
+db.save(flight_bucket, 'DLH-4001', {code: 'DLH-4001', to: 'JFK', from: 'MUC', departure: 'Tue, 23 Aug 2010 13:30:00 GMT'})();
+db.save(flight_bucket, 'AMX-1344', {code: 'AMX-1344', to: 'EZE', from: 'MEX', departure: 'Wed, 21 Jul 2010 08:45:00 GMT'})();
+db.save(flight_bucket, 'AMX-1346', {code: 'AMX-1346', to: 'MEX', from: 'EZE', departure: 'Mon, 08 Mar 2010 19:40:00 GMT'})();
+db.save(flight_bucket, 'KLM-1196', {code: 'KLM-1196', to: 'AMS', from: 'CDG', departure: 'Fri, 20 Aug 2010 14:59:00 GMT'})();
+db.save(flight_bucket, 'CPA-729', {code: 'CPA-729', to: 'CDG', from: 'HKK', departure: 'Thu, 19 Aug 2010 07:30:00 GMT'})();
+db.save(flight_bucket, 'ARG-909', {code: 'ARG-909', to: 'AMS', from: 'EZE', departure: 'Tue, 24 Aug 2010 15:25:00 GMT'})();
+db.save(flight_bucket, 'IBE-4418', {code: 'IBE-4418', to: 'BCN', from: 'JFK', departure: 'Sat, 24 Jul 2010 12:00:00 GMT'})();
+
+var klm_header = {headers: {link: db.makeLinks([
+ { bucket: flight_bucket, key: 'KLM-8098', tag: 'flight' },
+ { bucket: flight_bucket, key: 'KLM-1196', tag: 'flight' }
+])}};
+
+var klm2 = {headers: {link: db.makeLinks([{ bucket: airport_bucket, key: 'AMS', tag: 'base'},
+ { bucket: airport_bucket, key: 'CDG', tag: 'base' }
+])}};
+
+db.save(airline_bucket, 'KLM', {name: 'KLM', fleet: 111, alliance: 'SkyTeam', european: true}, klm_header)();
+db.save(airline_bucket, 'AFR', {name: 'Air France', fleet: 263, alliance: 'SkyTeam', european: true})();
+db.save(airline_bucket, 'AMX', {name: 'Aeroméxico', fleet: 43, alliance: 'SkyTeam', european: false})();
+db.save(airline_bucket, 'ARG', {name: 'Aerolíneas Argentinas', fleet: 40, european: false})();
+db.save(airline_bucket, 'DLH', {name: 'Lufthansa', fleet: 262, alliance: 'Star Alliance', european: true})();
+db.save(airline_bucket, 'IBE', {name: 'Iberia', fleet: 183, alliance: 'One World', european: true})();
+db.save(airline_bucket, 'CPA', {name: 'Cathay Pacific', fleet: 127, alliance: 'One World', european: false})();
+
+// querying
+
+var map = function(v, keydata, args) {
+ if (v.values) {
+ var ret = [], a = Riak.mapValuesJson(v)[0];
+ if ((a.from === args.from) && (new Date(a.departure) < new Date(args.before) )) {
+ ret.push(a);
+ }
+ return ret;
+ } else {
+ return [];
+ }
+};
+
+var from = 'EZE', before = 'Wed, 14 Jul 2010',
+ query = {
+ inputs: flight_bucket,
+ query: [ {map: {source: map, arg: {from: from, before: before}}} ]
+ };
+
+var flight_print = function(flight) {
+ db.log('Flight ' + flight.code + ' with destination ' + flight.to + ' departing ' + flight.departure);
+};
+
+db.mapReduce(query)(function(response) {
+ assert.equal(response.length, 2);
+ db.log('Flights from ' + from + ' before ' + before + ':');
+ response.forEach(flight_print);
+ db.log("");
+});
+
+// link-walking
+
+db.walk(airline_bucket, 'KLM', [["_", "flight"]])(function(response) {
+ assert.equal(response.length, 2);
+ db.log('Flights for airline KLM:');
+ response.forEach(flight_print);
+ db.log("");
+});
+
+// cleanup
+
+[airline_bucket, airport_bucket, flight_bucket].forEach(function(bucket) {
+ db.get(bucket)(function(response) {
+ response.keys.forEach(function(key) {
+ db.remove(bucket, key)(function(){});
+ })
+ })
+})
59 test/riak-test.js
@@ -0,0 +1,59 @@
+// This file is provided to you under the Apache License,
+// Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+var sys = require('sys'),
+ Riak = require('../riak-node')
+ assert = require('assert');
+
+var db = new Riak.Client(),
+ bucket = 'riak-js-random-bucket',
+ doc = "test",
+ doc_json = "test-json",
+ content = "this is a test",
+ content_json = { a: 1, b: "test", c: false };
+
+db.save(bucket, doc, content, {returnbody: true})(function(response, meta) {
+ assert.ok(response);
+ assert.notEqual(204, meta.statusCode);
+
+ db.get(bucket, doc)(function(response2) {
+ assert.equal(response2, content);
+ db.remove(bucket, doc)(function() {
+ db.get(bucket, doc)(null, function(r, meta3) {
+ assert.equal(404, meta3.statusCode);
+ });
+ });
+ });
+});
+
+db.save(bucket, doc_json, content_json)(function(response, meta) {
+ assert.equal(204, meta.statusCode);
+ db.get(bucket, doc_json)(function(response2, meta2) {
+ // check objects and meta (headers)
+ // deepEqual response
+ });
+});
+
+
+// wait 2 seconds and delete all documents from the test bucket no matter what
+var nothing = function() {};
+
+var func = function() { db.get(bucket)(function(resp) {
+ resp.keys.forEach(function(key) {
+ db.remove(bucket, key)();
+ });
+ });
+}
+
+setTimeout(func, 2000);

0 comments on commit 9a45311

Please sign in to comment.