Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Compile js from coffee

  • Loading branch information...
commit fe972f0286c9d520aa9149db5731ed96271da8de 1 parent 7b50c4f
@wdavidw authored
View
12 Makefile
@@ -1,11 +1,19 @@
REPORTER = dot
-doc:
+build:
+ @./node_modules/.bin/coffee -b -o lib src/*.coffee
+
+doc: build
@./node_modules/.bin/coffee lib/doc $(RON_DOC)
-test:
+test: build
@NODE_ENV=test ./node_modules/.bin/mocha --compilers coffee:coffee-script \
--reporter $(REPORTER)
+coverage: build
+ @jscoverage --no-highlight lib lib-cov
+ @RON_COV=1 $(MAKE) test REPORTER=html-cov > doc/coverage.html
+ @rm -rf lib-cov
+
.PHONY: test
View
162 lib/Client.js
@@ -0,0 +1,162 @@
+// Generated by CoffeeScript 1.4.0
+var Client, Records, Schema, redis;
+
+redis = require('redis');
+
+Schema = require('./Schema');
+
+Records = require('./Records');
+
+/*
+
+Client connection
+=================
+
+The client wraps a redis connection and provides access to records definition
+and manipulation.
+
+Internally, Ron use the [Redis client for Node.js](https://github.com/mranney/node_redis).
+*/
+
+
+module.exports = Client = (function() {
+ /*
+
+ `ron([options])` Client creation
+ --------------------------------
+
+ `options` Options properties include:
+
+ * `name` A namespace for the application, all keys with be prefixed with "#{name}:". Default to "ron"
+ * `redis` Provide an existing instance in case you don't want a new one to be created.
+ * `host` Redis hostname.
+ * `port` Redis port.
+ * `password` Redis password.
+ * `database` Redis database (an integer).
+
+ Basic example:
+
+ ron = require 'ron'
+ client = ron
+ host: '127.0.0.1'
+ port: 6379
+ */
+
+ function Client(options) {
+ var _ref, _ref1;
+ if (options == null) {
+ options = {};
+ }
+ this.options = options;
+ this.name = options.name || 'ron';
+ this.schemas = {};
+ this.records = {};
+ if (this.options.redis) {
+ this.redis = this.options.redis;
+ } else {
+ this.redis = redis.createClient((_ref = options.port) != null ? _ref : 6379, (_ref1 = options.host) != null ? _ref1 : '127.0.0.1');
+ if (options.password != null) {
+ this.redis.auth(options.password);
+ }
+ if (options.database != null) {
+ this.redis.select(options.database);
+ }
+ }
+ }
+
+ /*
+
+ `get(schema)` Records definition and access
+ -------------------------------------------
+ Return a records instance. If the `schema` argument is an object, a new
+ instance will be created overwriting any previously defined instance
+ with the same name.
+
+ `schema` An object defining a new schema or a string referencing a schema name.
+
+ Define a record from a object:
+
+ client.get
+ name: 'users'
+ properties:
+ user_id: identifier: true
+ username: unique: true
+ email: index: true
+
+ Define a record from function calls:
+
+ Users = client.get 'users'
+ Users.identifier 'user_id'
+ Users.unique 'username'
+ Users.index 'email'
+
+ Alternatively, the function could be called with a string
+ followed by multiple schema definition that will be merged.
+ Here is a valid example:
+
+ client.get 'username', temporal: true, properties: username: unique: true
+ */
+
+
+ Client.prototype.get = function(schema) {
+ var create, i, k, v, _i, _ref, _ref1;
+ create = true;
+ if (arguments.length > 1) {
+ if (typeof arguments[0] === 'string') {
+ schema = {
+ name: arguments[0]
+ };
+ } else {
+ schema = arguments[0];
+ }
+ for (i = _i = 1, _ref = arguments.length; 1 <= _ref ? _i < _ref : _i > _ref; i = 1 <= _ref ? ++_i : --_i) {
+ _ref1 = arguments[i];
+ for (k in _ref1) {
+ v = _ref1[k];
+ schema[k] = v;
+ }
+ }
+ } else if (typeof schema === 'string') {
+ schema = {
+ name: schema
+ };
+ if (this.records[schema.name] != null) {
+ create = false;
+ }
+ }
+ if (create) {
+ this.records[schema.name] = new Records(this, schema);
+ }
+ return this.records[schema.name];
+ };
+
+ /*
+
+ `quit(callback)` Quit
+ ---------------------
+ Destroy the redis connection.
+
+ `callback` Received parameters are:
+
+ * `err` Error object if any.
+ * `status` Status provided by the redis driver
+ */
+
+
+ Client.prototype.quit = function(callback) {
+ return this.redis.quit(function(err, status) {
+ if (!callback) {
+ return;
+ }
+ if (err) {
+ return callback(err);
+ }
+ if (callback) {
+ return callback(null, status);
+ }
+ });
+ };
+
+ return Client;
+
+})();
View
1,096 lib/Records.js
@@ -0,0 +1,1096 @@
+// Generated by CoffeeScript 1.4.0
+var Records, Schema,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ __slice = [].slice;
+
+Schema = require('./Schema');
+
+/*
+
+Records access and manipulation
+===============================
+
+Implement object based storage with indexing support.
+
+Identifier
+----------
+
+Auto generated identifiers are incremented integers. The next identifier is obtained from
+a key named as `{s.db}:{s.name}_incr`. All the identifiers are stored as a Redis set in
+a key named as `{s.db}:{s.name}_#{identifier}`.
+
+Data
+----
+
+Records data is stored as a single hash named as `{s.db}:{s.name}:{idenfitier}`. The hash
+keys map to the record properties and the hash value map to the values associated with
+each properties.
+
+Regular indexes
+---------------
+
+Regular index are stored inside multiple sets, named as
+`{s.db}:{s.name}_{property}:{value}`. There is one key for each indexed value and its
+associated value is a set containing all the identifiers of the records whose property
+match the indexed value.
+
+Unique indexes
+--------------
+
+Unique indexes are stored inside a single hash key named as
+`{s.db}:{s.name}_{property}`. Inside the hash, keys are the unique values
+associated to the indexed property and values are the record identifiers.
+*/
+
+
+module.exports = Records = (function(_super) {
+
+ __extends(Records, _super);
+
+ function Records(ron, schema) {
+ this.redis = ron.redis;
+ Records.__super__.constructor.call(this, ron, schema);
+ }
+
+ /*
+
+ `all(callback)`
+ ---------------
+ Return all records. Similar to the find method with far less options
+ and a faster implementation.
+ */
+
+
+ Records.prototype.all = function(callback) {
+ var db, identifier, name, redis, _ref,
+ _this = this;
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier;
+ return redis.smembers("" + db + ":" + name + "_" + identifier, function(err, recordIds) {
+ var multi, recordId, _i, _len;
+ multi = redis.multi();
+ for (_i = 0, _len = recordIds.length; _i < _len; _i++) {
+ recordId = recordIds[_i];
+ multi.hgetall("" + db + ":" + name + ":" + recordId);
+ }
+ return multi.exec(function(err, records) {
+ if (err) {
+ return callback(err);
+ }
+ _this.unserialize(records);
+ return callback(null, records);
+ });
+ });
+ };
+
+ /*
+
+ `clear(callback)`
+ -----------------
+ Remove all the records and the references poiting to them. This function
+ takes no other argument than the callback called on error or success.
+
+ `callback` Received parameters are:
+
+ * `err` Error object if any.
+ * `count` Number of removed records on success
+
+ Usage:
+
+ ron.get('users').clear (err, count) ->
+ return console.error "Failed: #{err.message}" if err
+ console.log "#{count} records removed"
+ */
+
+
+ Records.prototype.clear = function(callback) {
+ var cmds, count, db, hash, identifier, index, indexProperties, indexSort, multi, name, property, redis, unique, _i, _len, _ref;
+ redis = this.redis, hash = this.hash;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, index = _ref.index, unique = _ref.unique;
+ cmds = [];
+ count = 0;
+ multi = redis.multi();
+ indexSort = [];
+ indexProperties = Object.keys(index);
+ if (indexProperties.length) {
+ indexSort.push("" + db + ":" + name + "_" + identifier);
+ for (_i = 0, _len = indexProperties.length; _i < _len; _i++) {
+ property = indexProperties[_i];
+ indexSort.push('get');
+ indexSort.push("" + db + ":" + name + ":*->" + property);
+ cmds.push(['del', "" + db + ":" + name + "_" + property + ":null"]);
+ }
+ indexSort.push(function(err, values) {
+ var i, j, value, _j, _ref1, _ref2, _results;
+ if (values.length) {
+ _results = [];
+ for (i = _j = 0, _ref1 = values.length, _ref2 = indexProperties.length; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; i = _j += _ref2) {
+ _results.push((function() {
+ var _k, _len1, _results1;
+ _results1 = [];
+ for (j = _k = 0, _len1 = indexProperties.length; _k < _len1; j = ++_k) {
+ property = indexProperties[j];
+ value = hash(values[i + j]);
+ _results1.push(cmds.push(['del', "" + db + ":" + name + "_" + property + ":" + value]));
+ }
+ return _results1;
+ })());
+ }
+ return _results;
+ }
+ });
+ multi.sort.apply(multi, indexSort);
+ }
+ multi.smembers("" + db + ":" + name + "_" + identifier, function(err, recordIds) {
+ var recordId, _j, _len1, _results;
+ if (err) {
+ return callback(err);
+ }
+ if (recordIds == null) {
+ recordIds = [];
+ }
+ count = recordIds.length;
+ for (_j = 0, _len1 = recordIds.length; _j < _len1; _j++) {
+ recordId = recordIds[_j];
+ cmds.push(['del', "" + db + ":" + name + ":" + recordId]);
+ }
+ cmds.push(['del', "" + db + ":" + name + "_incr"]);
+ cmds.push(['del', "" + db + ":" + name + "_" + identifier]);
+ for (property in unique) {
+ cmds.push(['del', "" + db + ":" + name + "_" + property]);
+ }
+ _results = [];
+ for (property in index) {
+ _results.push(cmds.push(['del', "" + db + ":" + name + "_" + property]));
+ }
+ return _results;
+ });
+ return multi.exec(function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ multi = redis.multi(cmds);
+ return multi.exec(function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, count);
+ });
+ });
+ };
+
+ /*
+
+ `count(callback)`
+ -----------------
+ Count the number of records present in the database.
+
+ Counting all the records:
+
+ Users.count, (err, count) ->
+ console.log 'count users', count
+
+ `count(property, values, callback)`
+ ----------------------------------
+ Count the number of one or more values for an indexed property.
+
+ Counting multiple values:
+
+ Users.get 'users', properties:
+ user_id: identifier: true
+ job: index: true
+ Users.count 'job' [ 'globtrotter', 'icemaker' ], (err, counts) ->
+ console.log 'count globtrotter', counts[0]
+ console.log 'count icemaker', counts[1]
+ */
+
+
+ Records.prototype.count = function(callback) {
+ var db, i, identifier, index, isArray, multi, name, property, redis, value, values, _i, _len, _ref;
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, index = _ref.index;
+ if (arguments.length === 3) {
+ property = callback;
+ values = arguments[1];
+ callback = arguments[2];
+ if (!index[property]) {
+ return callback(new Error("Property is not indexed"));
+ }
+ isArray = Array.isArray(values);
+ if (!isArray) {
+ values = [values];
+ }
+ multi = redis.multi();
+ for (i = _i = 0, _len = values.length; _i < _len; i = ++_i) {
+ value = values[i];
+ value = this.hash(value);
+ multi.scard("" + db + ":" + name + "_" + property + ":" + value);
+ }
+ return multi.exec(function(err, counts) {
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, isArray ? counts : counts[0]);
+ });
+ } else {
+ return this.redis.scard("" + db + ":" + name + "_" + identifier, function(err, count) {
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, count);
+ });
+ }
+ };
+
+ /*
+
+ `create(records, [options], callback)`
+ --------------------------------------
+ Insert one or multiple record. The records must not already exists
+ in the database or an error will be returned in the callback. Only
+ the defined properties are inserted.
+
+ The records passed to the function are returned in the callback enriched their new identifier property.
+
+ `records` Record object or array of record objects.
+
+ `options` Options properties include:
+
+ * `identifiers` Return only the created identifiers instead of the records.
+ * `validate` Validate the records.
+ * `properties` Array of properties to be returned.
+ * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
+ * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if any.
+ * `records` Records with their newly created identifier.
+
+ Records are not validated, it is the responsability of the client program calling `create` to either
+ call `validate` before calling `create` or to passs the `validate` options.
+ */
+
+
+ Records.prototype.create = function(records, options, callback) {
+ var db, hash, identifier, index, isArray, name, properties, redis, temporal, unique, _ref,
+ _this = this;
+ if (arguments.length === 2) {
+ callback = options;
+ options = {};
+ }
+ redis = this.redis, hash = this.hash;
+ _ref = this.data, db = _ref.db, name = _ref.name, temporal = _ref.temporal, properties = _ref.properties, identifier = _ref.identifier, index = _ref.index, unique = _ref.unique;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ if (options.validate) {
+ try {
+ this.validate(records, {
+ "throw": true
+ });
+ } catch (e) {
+ return callback(e, (isArray ? records : records[0]));
+ }
+ }
+ return this.exists(records, function(err, recordIds) {
+ var date, multi, recordId, _i, _len;
+ if (err) {
+ return callback(err);
+ }
+ for (_i = 0, _len = recordIds.length; _i < _len; _i++) {
+ recordId = recordIds[_i];
+ if (recordId != null) {
+ return callback(new Error("Record " + recordId + " already exists"));
+ }
+ }
+ multi = redis.multi();
+ if (temporal != null) {
+ date = new Date(Date.now());
+ }
+ return _this.id(records, function(err, records) {
+ var i, property, r, record, value, _j, _len1;
+ multi = redis.multi();
+ for (i = _j = 0, _len1 = records.length; _j < _len1; i = ++_j) {
+ record = records[i];
+ if (((temporal != null ? temporal.creation : void 0) != null) && !(record[temporal.creation] != null)) {
+ record[temporal.creation] = date;
+ }
+ if (((temporal != null ? temporal.modification : void 0) != null) && !(record[temporal.modification] != null)) {
+ record[temporal.modification] = date;
+ }
+ multi.sadd("" + db + ":" + name + "_" + identifier, record[identifier]);
+ for (property in unique) {
+ if (record[property]) {
+ multi.hset("" + db + ":" + name + "_" + property, record[property], record[identifier]);
+ }
+ }
+ for (property in index) {
+ value = record[property];
+ value = hash(value);
+ multi.sadd("" + db + ":" + name + "_" + property + ":" + value, record[identifier]);
+ }
+ r = {};
+ for (property in record) {
+ value = record[property];
+ if (!properties[property]) {
+ continue;
+ }
+ if (value != null) {
+ r[property] = value;
+ }
+ }
+ _this.serialize(r);
+ multi.hmset("" + db + ":" + name + ":" + record[identifier], r);
+ }
+ return multi.exec(function(err, results) {
+ var result, _k, _len2;
+ if (err) {
+ return callback(err);
+ }
+ for (_k = 0, _len2 = results.length; _k < _len2; _k++) {
+ result = results[_k];
+ if (result[0] === !"0") {
+ return callback(new Error('Corrupted user database '));
+ }
+ }
+ _this.unserialize(records, options);
+ return callback(null, isArray ? records : records[0]);
+ });
+ });
+ });
+ };
+
+ /*
+
+ `exists(records, callback)`
+ ---------------------------
+ Check if one or more record exist. The existence of a record is based on its
+ id or any property defined as unique. The provided callback is called with
+ an error or the records identifiers. The identifiers respect the same
+ structure as the provided records argument. If a record does not exists,
+ its associated return value is null.
+
+ `records` Record object or array of record objects.
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if any.
+ * `identifier` Record identifiers or null values.
+ */
+
+
+ Records.prototype.exists = function(records, callback) {
+ var db, identifier, isArray, multi, name, property, record, recordId, redis, unique, _i, _len, _ref,
+ _this = this;
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, unique = _ref.unique;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ multi = redis.multi();
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ if (typeof record === 'object') {
+ if (record[identifier] != null) {
+ recordId = record[identifier];
+ multi.hget("" + db + ":" + name + ":" + recordId, identifier);
+ } else {
+ for (property in unique) {
+ if (record[property] != null) {
+ multi.hget("" + db + ":" + name + "_" + property, record[property]);
+ }
+ }
+ }
+ } else {
+ multi.hget("" + db + ":" + name + ":" + record, identifier);
+ }
+ }
+ return multi.exec(function(err, recordIds) {
+ if (err) {
+ return callback(err);
+ }
+ _this.unserialize(recordIds);
+ return callback(null, isArray ? recordIds : recordIds[0]);
+ });
+ };
+
+ /*
+
+ `get(records, [options], callback)`
+ -----------------------------------
+ Retrieve one or multiple records. Records that doesn't exists are returned as null. If
+ options is an array, it is considered to be the list of properties to retrieve. By default,
+ unless the `force` option is defined, only the properties not yet defined in the provided
+ records are fetched from Redis.
+
+ `options` All options are optional. Options properties include:
+
+ * `properties` Array of properties to fetch, all properties unless defined.
+ * `force` Force the retrieval of properties even if already present in the record objects.
+ * `accept_null` Skip objects if they are provided as null.
+ * `object` If `true`, return an object where keys are the identifier and value are the fetched records
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if the command failed.
+ * `records` Object or array of object if command succeed. Objects are null if records are not found.
+ */
+
+
+ Records.prototype.get = function(records, options, callback) {
+ var db, identifier, isArray, name, redis, _ref,
+ _this = this;
+ if (arguments.length === 2) {
+ callback = options;
+ options = {};
+ }
+ if (Array.isArray(options)) {
+ options = {
+ properties: options
+ };
+ }
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ if ((options.accept_null != null) && !records.some(function(record) {
+ return record !== null;
+ })) {
+ return callback(null, isArray ? records : records[0]);
+ }
+ return this.identify(records, {
+ object: true,
+ accept_null: options.accept_null != null
+ }, function(err, records) {
+ var cmds, multi;
+ if (err) {
+ return callback(err);
+ }
+ cmds = [];
+ records.forEach(function(record, i) {
+ var recordId, _ref1;
+ if (record == null) {
+ return;
+ }
+ recordId = record[identifier];
+ if (recordId === null) {
+ records[i] = null;
+ } else if ((_ref1 = options.properties) != null ? _ref1.length : void 0) {
+ options.properties.forEach(function(property) {
+ if (!(options.force || record[property])) {
+ return cmds.push([
+ 'hget', "" + db + ":" + name + ":" + recordId, property, function(err, value) {
+ return record[property] = value;
+ }
+ ]);
+ }
+ });
+ } else {
+ cmds.push([
+ 'hgetall', "" + db + ":" + name + ":" + recordId, function(err, values) {
+ var property, value, _results;
+ _results = [];
+ for (property in values) {
+ value = values[property];
+ _results.push(record[property] = value);
+ }
+ return _results;
+ }
+ ]);
+ }
+ return cmds.push([
+ 'exists', "" + db + ":" + name + ":" + recordId, function(err, exists) {
+ if (!exists) {
+ return records[i] = null;
+ }
+ }
+ ]);
+ });
+ if (cmds.length === 0) {
+ return callback(null, isArray ? records : records[0]);
+ }
+ multi = redis.multi(cmds);
+ return multi.exec(function(err, values) {
+ var record, recordsByIds, _i, _len;
+ if (err) {
+ return callback(err);
+ }
+ _this.unserialize(records);
+ if (options.object) {
+ recordsByIds = {};
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ recordsByIds[record[identifier]] = record;
+ }
+ return callback(null, recordsByIds);
+ } else {
+ return callback(null, isArray ? records : records[0]);
+ }
+ });
+ });
+ };
+
+ /*
+ `id(records, callback)`
+ -----------------------
+ Generate new identifiers. The first arguments `records` may be the number
+ of ids to generate, a record or an array of records.
+ */
+
+
+ Records.prototype.id = function(records, callback) {
+ var db, i, identifier, incr, isArray, name, record, redis, unique, _i, _len, _ref,
+ _this = this;
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, unique = _ref.unique;
+ if (typeof records === 'number') {
+ incr = records;
+ isArray = true;
+ records = (function() {
+ var _i, _results;
+ _results = [];
+ for (i = _i = 0; 0 <= records ? _i < records : _i > records; i = 0 <= records ? ++_i : --_i) {
+ _results.push(null);
+ }
+ return _results;
+ })();
+ } else {
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ incr = 0;
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ if (!record[identifier]) {
+ incr++;
+ }
+ }
+ }
+ return redis.incrby("" + db + ":" + name + "_incr", incr, function(err, recordId) {
+ var _j, _len1;
+ recordId = recordId - incr;
+ if (err) {
+ return callback(err);
+ }
+ for (i = _j = 0, _len1 = records.length; _j < _len1; i = ++_j) {
+ record = records[i];
+ if (!record) {
+ records[i] = record = {};
+ }
+ if (!record[identifier]) {
+ recordId++;
+ }
+ if (!record[identifier]) {
+ record[identifier] = recordId;
+ }
+ }
+ return callback(null, isArray ? records : records[0]);
+ });
+ };
+
+ /*
+
+ `identify(records, [options], callback)`
+ ----------------------------------------
+ Extract record identifiers or set the identifier to null if its associated record could not be found.
+
+ The method doesn't hit the database to validate record values and if an id is
+ provided, it wont check its existence. When a record has no identifier but a unique value, then its
+ identifier will be fetched from Redis.
+
+ `records` Record object or array of record objects.
+
+ `options` Options properties include:
+
+ * `accept_null` Skip objects if they are provided as null.
+ * `object` Return an object in the callback even if it recieve an id instead of a record.
+
+ Use reverse index lookup to extract user ids:
+
+ Users.get 'users', properties:
+ user_id: identifier: true
+ username: unique: true
+ Users.id [
+ {username: 'username_1'}
+ {username: 'username_2'}
+ ], (err, ids) ->
+ should.not.exist err
+ console.log ids
+
+ Use the `object` option to return records instead of ids:
+
+ Users.get 'users', properties:
+ user_id: identifier: true
+ username: unique: true
+ Users.id [
+ 1, {user_id: 2} ,{username: 'username_3'}
+ ], object: true, (err, users) ->
+ should.not.exist err
+ ids = for user in users then user.user_id
+ console.log ids
+ */
+
+
+ Records.prototype.identify = function(records, options, callback) {
+ var cmds, db, err, finalize, i, identifier, isArray, multi, name, property, record, redis, unique, withUnique, _i, _len, _ref,
+ _this = this;
+ if (arguments.length === 2) {
+ callback = options;
+ options = {};
+ }
+ redis = this.redis;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, unique = _ref.unique;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ cmds = [];
+ err = null;
+ for (i = _i = 0, _len = records.length; _i < _len; i = ++_i) {
+ record = records[i];
+ if (typeof record === 'object') {
+ if (record == null) {
+ if (!options.accept_null) {
+ return callback(new Error('Null record'));
+ }
+ } else if (record[identifier] != null) {
+
+ } else {
+ withUnique = false;
+ for (property in unique) {
+ if (record[property] != null) {
+ withUnique = true;
+ cmds.push([
+ 'hget', "" + db + ":" + name + "_" + property, record[property], (function(record) {
+ return function(err, recordId) {
+ return record[identifier] = recordId;
+ };
+ })(record)
+ ]);
+ }
+ }
+ if (!withUnique) {
+ return callback(new Error('Invalid record, got ' + (JSON.stringify(record))));
+ }
+ }
+ } else if (typeof record === 'number' || typeof record === 'string') {
+ records[i] = {};
+ records[i][identifier] = record;
+ } else {
+ return callback(new Error('Invalid id, got ' + (JSON.stringify(record))));
+ }
+ }
+ finalize = function() {
+ if (!options.object) {
+ records = (function() {
+ var _j, _len1, _results;
+ _results = [];
+ for (_j = 0, _len1 = records.length; _j < _len1; _j++) {
+ record = records[_j];
+ if (record != null) {
+ _results.push(record[identifier]);
+ } else {
+ _results.push(record);
+ }
+ }
+ return _results;
+ })();
+ }
+ return callback(null, isArray ? records : records[0]);
+ };
+ if (cmds.length === 0) {
+ return finalize();
+ }
+ multi = redis.multi(cmds);
+ return multi.exec(function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ _this.unserialize(records);
+ return finalize();
+ });
+ };
+
+ /*
+
+ `list([options], callback)`
+ ---------------------------
+ List records with support for filtering and sorting.
+
+ `options` Options properties include:
+
+ * `direction` One of `asc` or `desc`, default to `asc`.
+ * `identifiers` Return an array of identifiers instead of the record objects.
+ * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
+ * `properties` Array of properties to be returned.
+ * `operation` Redis operation in case of multiple `where` properties, default to `union`.
+ * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
+ * `sort` Name of the property by which records should be ordered.
+ * `where` Hash of property/value used to filter the query.
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if any.
+ * `records` Records fetched from Redis.
+
+ Using the `union` operation:
+
+ Users.list
+ where: group: ['admin', 'redis']
+ operation: 'union'
+ direction: 'desc'
+ , (err, users) ->
+ console.log users
+
+ An alternative syntax is to bypass the `where` option, the exemple above
+ could be rewritten as:
+
+ Users.list
+ group: ['admin', 'redis']
+ operation: 'union'
+ direction: 'desc'
+ , (err, users) ->
+ console.log users
+ */
+
+
+ Records.prototype.list = function(options, callback) {
+ var args, db, filter, hash, identifier, index, keys, multi, name, operation, property, redis, tempkey, v, value, where, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3, _ref4,
+ _this = this;
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+ redis = this.redis, hash = this.hash;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, index = _ref.index;
+ options.properties = options.properties || Object.keys(this.data.properties);
+ if (options.identifiers) {
+ options.properties = [identifier];
+ }
+ args = [];
+ multi = this.redis.multi();
+ if (options.where == null) {
+ options.where = {};
+ }
+ where = [];
+ for (property in options) {
+ value = options[property];
+ if (index[property]) {
+ if (Array.isArray(value)) {
+ for (_i = 0, _len = value.length; _i < _len; _i++) {
+ v = value[_i];
+ where.push([property, v]);
+ }
+ } else {
+ where.push([property, value]);
+ }
+ }
+ }
+ options.where = Object.keys(options.where).length ? options.where : false;
+ if (where.length === 1) {
+ _ref1 = where[0], property = _ref1[0], value = _ref1[1];
+ value = hash(value);
+ args.push("" + db + ":" + name + "_" + property + ":" + value);
+ } else if (where.length > 1) {
+ tempkey = "temp:" + ((new Date).getTime()) + (Math.random());
+ keys = [];
+ keys.push(tempkey);
+ args.push(tempkey);
+ for (_j = 0, _len1 = where.length; _j < _len1; _j++) {
+ filter = where[_j];
+ property = filter[0], value = filter[1];
+ value = hash(value);
+ keys.push("" + db + ":" + name + "_" + property + ":" + value);
+ }
+ operation = (_ref2 = options.operation) != null ? _ref2 : 'union';
+ multi["s" + operation + "store"].apply(multi, keys);
+ } else {
+ args.push("" + db + ":" + name + "_" + identifier);
+ }
+ if (options.sort != null) {
+ args.push('by');
+ args.push(("" + db + ":" + name + ":*->") + options.sort);
+ }
+ _ref3 = options.properties;
+ for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
+ property = _ref3[_k];
+ args.push('get');
+ args.push(("" + db + ":" + name + ":*->") + property);
+ }
+ args.push('alpha');
+ args.push((_ref4 = options.direction) != null ? _ref4 : 'asc');
+ args.push(function(err, values) {
+ var i, j, record, result;
+ if (err) {
+ return callback(err);
+ }
+ if (!values.length) {
+ return callback(null, []);
+ }
+ result = (function() {
+ var _l, _len3, _m, _ref5, _ref6, _ref7, _results;
+ _results = [];
+ for (i = _l = 0, _ref5 = values.length, _ref6 = options.properties.length; 0 <= _ref5 ? _l < _ref5 : _l > _ref5; i = _l += _ref6) {
+ record = {};
+ _ref7 = options.properties;
+ for (j = _m = 0, _len3 = _ref7.length; _m < _len3; j = ++_m) {
+ property = _ref7[j];
+ record[property] = values[i + j];
+ }
+ _results.push(this.unserialize(record, options));
+ }
+ return _results;
+ }).call(_this);
+ return callback(null, result);
+ });
+ multi.sort.apply(multi, args);
+ if (tempkey) {
+ multi.del(tempkey);
+ }
+ return multi.exec();
+ };
+
+ /*
+
+ `remove(records, callback)`
+ ---------------------------
+ Remove one or several records from the database. The function will also
+ handle all the indexes referencing those records.
+
+ `records` Record object or array of record objects.
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if any.
+ * `removed` Number of removed records.
+
+ Removing a single record:
+
+ Users.remove id, (err, removed) ->
+ console.log "#{removed} user removed"
+ */
+
+
+ Records.prototype.remove = function(records, callback) {
+ var db, hash, identifier, index, isArray, name, redis, removed, unique, _ref;
+ redis = this.redis, hash = this.hash;
+ _ref = this.data, db = _ref.db, name = _ref.name, identifier = _ref.identifier, index = _ref.index, unique = _ref.unique;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ removed = 0;
+ return this.get(records, [].concat(Object.keys(unique), Object.keys(index)), function(err, records) {
+ var multi, record, _fn, _i, _len;
+ if (err) {
+ return callback(err);
+ }
+ multi = redis.multi();
+ _fn = function(record) {
+ var property, recordId, value, _results;
+ recordId = record[identifier];
+ multi.del("" + db + ":" + name + ":" + recordId, function(err) {
+ return removed++;
+ });
+ multi.srem("" + db + ":" + name + "_" + identifier, recordId);
+ for (property in unique) {
+ multi.hdel("" + db + ":" + name + "_" + property, record[property]);
+ }
+ _results = [];
+ for (property in index) {
+ value = hash(record[property]);
+ _results.push(multi.srem("" + db + ":" + name + "_" + property + ":" + value, recordId, function(err, count) {
+ if (count !== 1) {
+ return console.warn('Missing indexed property');
+ }
+ }));
+ }
+ return _results;
+ };
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ if (record === null) {
+ continue;
+ }
+ _fn(record);
+ }
+ return multi.exec(function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, removed);
+ });
+ });
+ };
+
+ /*
+
+ `update(records, [options], callback)`
+ --------------------------------------
+ Update one or several records. The records must exists in the database or
+ an error will be returned in the callback. The existence of a record may
+ be discovered through its identifier or the presence of a unique property.
+
+ `records` Record object or array of record objects.
+
+ `options` Options properties include:
+
+ * `validate` Validate the records.
+
+ `callback` Called on success or failure. Received parameters are:
+
+ * `err` Error object if any.
+ * `records` Records with their newly created identifier.
+
+ Records are not validated, it is the responsability of the client program to either
+ call `validate` before calling `update` or to passs the `validate` options.
+
+ Updating a single record:
+
+ Users.update
+ username: 'my_username'
+ age: 28
+ , (err, user) -> console.log user
+ */
+
+
+ Records.prototype.update = function(records, options, callback) {
+ var db, hash, identifier, index, isArray, name, properties, redis, temporal, unique, _ref,
+ _this = this;
+ if (arguments.length === 2) {
+ callback = options;
+ options = {};
+ }
+ redis = this.redis, hash = this.hash;
+ _ref = this.data, db = _ref.db, name = _ref.name, temporal = _ref.temporal, properties = _ref.properties, identifier = _ref.identifier, unique = _ref.unique, index = _ref.index;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ if (options.validate) {
+ try {
+ this.validate(records, {
+ "throw": true,
+ skip_required: true
+ });
+ } catch (e) {
+ return callback(e, (isArray ? records : records[0]));
+ }
+ }
+ return this.identify(records, {
+ object: true
+ }, function(err, records) {
+ var cmdsCheck, cmdsUpdate, multi, property, r, record, recordId, value, _fn, _i, _j, _len, _len1;
+ if (err) {
+ return callback(err);
+ }
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ if (!record) {
+ return callback(new Error('Invalid record'));
+ }
+ }
+ cmdsCheck = [];
+ cmdsUpdate = [];
+ multi = redis.multi();
+ _fn = function(record) {
+ var potentiallyChangedProperties, property, recordId, _k, _len2, _ref1;
+ recordId = record[identifier];
+ potentiallyChangedProperties = [];
+ _ref1 = [].concat(Object.keys(unique), Object.keys(index));
+ for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
+ property = _ref1[_k];
+ if (typeof record[property] !== 'undefined') {
+ potentiallyChangedProperties.push(property);
+ }
+ }
+ if (potentiallyChangedProperties.length) {
+ return multi.hmget.apply(multi, ["" + db + ":" + name + ":" + recordId].concat(__slice.call(potentiallyChangedProperties), [function(err, values) {
+ var propertyI, valueNew, valueOld, _l, _len3, _results;
+ _results = [];
+ for (propertyI = _l = 0, _len3 = potentiallyChangedProperties.length; _l < _len3; propertyI = ++_l) {
+ property = potentiallyChangedProperties[propertyI];
+ if (values[propertyI] !== record[property]) {
+ if (properties[property].unique) {
+ cmdsCheck.push(['hexists', "" + db + ":" + name + "_" + property, record[property]]);
+ cmdsUpdate.push(['hdel', "" + db + ":" + name + "_" + property, values[propertyI]]);
+ _results.push(cmdsUpdate.push([
+ 'hsetnx', "" + db + ":" + name + "_" + property, record[property], recordId, function(err, success) {
+ if (!success) {
+ return console.warn('Trying to write on existing unique property');
+ }
+ }
+ ]));
+ } else if (properties[property].index) {
+ valueOld = hash(values[propertyI]);
+ valueNew = hash(record[property]);
+ cmdsUpdate.push(['srem', "" + db + ":" + name + "_" + property + ":" + valueOld, recordId]);
+ _results.push(cmdsUpdate.push(['sadd', "" + db + ":" + name + "_" + property + ":" + valueNew, recordId]));
+ } else {
+ _results.push(void 0);
+ }
+ } else {
+ _results.push(void 0);
+ }
+ }
+ return _results;
+ }]));
+ }
+ };
+ for (_j = 0, _len1 = records.length; _j < _len1; _j++) {
+ record = records[_j];
+ recordId = record[identifier];
+ if (!recordId) {
+ return callback(new Error('Unsaved record'));
+ }
+ if (((temporal != null ? temporal.modification : void 0) != null) && !(record[temporal.modification] != null)) {
+ record[temporal.modification] = new Date(Date.now());
+ }
+ r = {};
+ for (property in record) {
+ value = record[property];
+ if (value != null) {
+ r[property] = value;
+ } else {
+ cmdsUpdate.push(['hdel', "" + db + ":" + name + ":" + recordId, property]);
+ }
+ }
+ _this.serialize(r);
+ cmdsUpdate.push(['hmset', "" + db + ":" + name + ":" + recordId, r]);
+ _fn(record);
+ }
+ return multi.exec(function(err, values) {
+ multi = redis.multi(cmdsCheck);
+ return multi.exec(function(err, exists) {
+ var exist, _k, _len2;
+ if (err) {
+ return callback(err);
+ }
+ for (_k = 0, _len2 = exists.length; _k < _len2; _k++) {
+ exist = exists[_k];
+ if (exist !== 0) {
+ return callback(new Error('Unique value already exists'));
+ }
+ }
+ multi = redis.multi(cmdsUpdate);
+ return multi.exec(function(err, results) {
+ if (err) {
+ return callback(err);
+ }
+ return callback(null, isArray ? records : records[0]);
+ });
+ });
+ });
+ });
+ };
+
+ return Records;
+
+})(Schema);
View
473 lib/Schema.js
@@ -0,0 +1,473 @@
+// Generated by CoffeeScript 1.4.0
+var Schema, crypto, isEmail;
+
+crypto = require('crypto');
+
+isEmail = function(email) {
+ return /^[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+(\.[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+)*@[a-z0-9\-]+(\.[a-z0-9\-]+)*\.([a-z]{2,})$/.test(email);
+};
+
+/*
+
+Schema definition
+=================
+
+Schema is a mixin from which `Records` inherits. A schema is defined once
+and must no change. We dont support schema migration at the moment. The `Records`
+class inherit all the properties and method of the shema.
+
+`ron` Reference to the Ron instance
+
+`options` Schema definition. Options include:
+
+* `name` Name of the schema.
+* `properties` Properties definition, an object or an array.
+
+Record properties may be defined by the following keys:
+
+* `type` Use to cast the value inside Redis, one of `string`, `int`, `date` or `email`.
+* `identifier` Mark this property as the identifier, only one property may be an identifier.
+* `index` Create an index on the property.
+* `unique` Create a unique index on the property.
+* `temporal` Add creation and modification date transparently.
+
+Define a schema from a configuration object:
+
+ ron.get 'users', properties:
+ user_id: identifier: true
+ username: unique: true
+ password: true
+
+Define a schema with a declarative approach:
+
+ Users = ron.get 'users'
+ Users.indentifier 'user_id'
+ Users.unique 'username'
+ Users.property 'password'
+
+Whichever your style, you can then manipulate your records:
+
+ users = ron.get 'users'
+ users.list (err, users) -> console.log users
+*/
+
+
+module.exports = Schema = (function() {
+
+ function Schema(ron, options) {
+ var name, value, _ref;
+ this.ron = ron;
+ if (typeof options === 'string') {
+ options = {
+ name: options
+ };
+ }
+ this.data = {
+ db: ron.name,
+ name: options.name,
+ temporal: {},
+ properties: {},
+ identifier: null,
+ index: {},
+ unique: {}
+ };
+ if (options.temporal) {
+ this.temporal(options.temporal);
+ }
+ if (options.properties) {
+ _ref = options.properties;
+ for (name in _ref) {
+ value = _ref[name];
+ this.property(name, value);
+ }
+ }
+ }
+
+ /*
+
+ `hash(key)`
+ -------------
+ Utility function used when a redis key is created out of
+ uncontrolled character (like a string instead of an int).
+ */
+
+
+ Schema.prototype.hash = function(key) {
+ if (typeof key === 'number') {
+ key = "" + key;
+ }
+ if (key != null) {
+ return crypto.createHash('sha1').update(key).digest('hex');
+ } else {
+ return 'null';
+ }
+ };
+
+ /*
+
+ `identifier(property)`
+ ------------------------
+ Define a property as an identifier or return the record identifier if
+ called without any arguments. An identifier is a property which uniquely
+ define a record. Inside Redis, identifier values are stored in set.
+ */
+
+
+ Schema.prototype.identifier = function(property) {
+ if (property != null) {
+ if (this.data.properties[property] == null) {
+ this.data.properties[property] = {};
+ }
+ this.data.properties[property].type = 'int';
+ this.data.properties[property].identifier = true;
+ this.data.identifier = property;
+ return this;
+ } else {
+ return this.data.identifier;
+ }
+ };
+
+ /*
+
+ `index([property])`
+ -------------------
+ Define a property as indexable or return all index properties. An
+ indexed property allow records access by its property value. For example,
+ when using the `list` function, the search can be filtered such as returned
+ records match one or multiple values.
+
+ Calling this function without any argument will return an array with all the
+ indexed properties.
+
+ Example:
+
+ User.index 'email'
+ User.list { filter: { email: 'my@email.com' } }, (err, users) ->
+ console.log 'This user has the following accounts:'
+ for user in user
+ console.log "- #{user.username}"
+ */
+
+
+ Schema.prototype.index = function(property) {
+ if (property != null) {
+ if (this.data.properties[property] == null) {
+ this.data.properties[property] = {};
+ }
+ this.data.properties[property].index = true;
+ this.data.index[property] = true;
+ return this;
+ } else {
+ return Object.keys(this.data.index);
+ }
+ };
+
+ /*
+
+ `property(property, [schema])`
+ ------------------------------
+ Define a new property or overwrite the definition of an
+ existing property. If no schema is provide, return the
+ property information.
+
+ Calling this function with only the property argument will return the schema
+ information associated with the property.
+
+ It is possible to define a new property without any schema information by
+ providing an empty object.
+
+ Example:
+
+ User.property 'id', identifier: true
+ User.property 'username', unique: true
+ User.property 'email', { index: true, type: 'email' }
+ User.property 'name', {}
+ */
+
+
+ Schema.prototype.property = function(property, schema) {
+ if (schema != null) {
+ if (schema == null) {
+ schema = {};
+ }
+ schema.name = property;
+ this.data.properties[property] = schema;
+ if (schema.identifier) {
+ this.identifier(property);
+ }
+ if (schema.index) {
+ this.index(property);
+ }
+ if (schema.unique) {
+ this.unique(property);
+ }
+ return this;
+ } else {
+ return this.data.properties[property];
+ }
+ };
+
+ /*
+
+ `name()`
+ --------
+ Return the schema name of the current instance.
+
+ Using the function :
+ Users = client 'users', properties: username: unique: true
+ console.log Users.name() is 'users'
+ */
+
+
+ Schema.prototype.name = function() {
+ return this.data.name;
+ };
+
+ /*
+
+ `serialize(records)`
+ --------------------
+ Cast record values before their insertion into Redis.
+
+ Take a record or an array of records and update values with correct
+ property types.
+ */
+
+
+ Schema.prototype.serialize = function(records) {
+ var i, isArray, properties, property, record, value, _i, _len, _ref;
+ properties = this.data.properties;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ for (i = _i = 0, _len = records.length; _i < _len; i = ++_i) {
+ record = records[i];
+ if (record == null) {
+ continue;
+ }
+ if (typeof record === 'object') {
+ for (property in record) {
+ value = record[property];
+ if (((_ref = properties[property]) != null ? _ref.type : void 0) === 'date' && (value != null)) {
+ if (typeof value === 'number') {
+
+ } else if (typeof value === 'string') {
+ if (/^\d+$/.test(value)) {
+ record[property] = parseInt(value, 10);
+ } else {
+ record[property] = Date.parse(value);
+ }
+ } else if (typeof value === 'object' && value instanceof Date) {
+ record[property] = value.getTime();
+ }
+ }
+ }
+ }
+ }
+ if (isArray) {
+ return records;
+ } else {
+ return records[0];
+ }
+ };
+
+ /*
+
+ `temporal([options])`
+ ---------------------
+ Define or retrieve temporal definition. Marking a schema as
+ temporal will transparently add two new date properties, the
+ date when the record was created (by default "cdate"), and the date
+ when the record was last updated (by default "mdate").
+ */
+
+
+ Schema.prototype.temporal = function(temporal) {
+ if (temporal != null) {
+ if (temporal === true) {
+ temporal = {
+ creation: 'cdate',
+ modification: 'mdate'
+ };
+ }
+ this.data.temporal = temporal;
+ this.property(temporal.creation, {
+ type: 'date'
+ });
+ return this.property(temporal.modification, {
+ type: 'date'
+ });
+ } else {
+ return [this.data.temporal.creation, this.data.temporal.modification];
+ }
+ };
+
+ /*
+
+ `unique([property])`
+ --------------------
+ Define a property as unique or retrieve all the unique properties if no
+ argument is provided. An unique property is similar to a index
+ property but the index is stored inside a Redis hash. In addition to being
+ filterable, it could also be used as an identifer to access a record.
+
+ Example:
+
+ User.unique 'username'
+ User.get { username: 'me' }, (err, user) ->
+ console.log "This is #{user.username}"
+ */
+
+
+ Schema.prototype.unique = function(property) {
+ if (property != null) {
+ if (this.data.properties[property] == null) {
+ this.data.properties[property] = {};
+ }
+ this.data.properties[property].unique = true;
+ this.data.unique[property] = true;
+ return this;
+ } else {
+ return Object.keys(this.data.unique);
+ }
+ };
+
+ /*
+
+ `unserialize(records, [options])`
+ ---------------------------------
+ Cast record values to their correct type.
+
+ Take a record or an array of records and update values with correct
+ property types.
+
+ `options` Options include:
+
+ * `identifiers` Return an array of identifiers instead of the record objects.
+ * `properties` Array of properties to be returned.
+ * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
+ * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
+ */
+
+
+ Schema.prototype.unserialize = function(records, options) {
+ var i, identifier, isArray, properties, property, record, value, _i, _len, _ref, _ref1, _ref2;
+ if (options == null) {
+ options = {};
+ }
+ _ref = this.data, identifier = _ref.identifier, properties = _ref.properties;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ if (options.identifiers) {
+ options.properties = [identifier];
+ }
+ for (i = _i = 0, _len = records.length; _i < _len; i = ++_i) {
+ record = records[i];
+ if (record == null) {
+ continue;
+ }
+ if (typeof record === 'object') {
+ for (property in record) {
+ value = record[property];
+ if (options.properties && options.properties.indexOf(property) === -1) {
+ delete record[property];
+ continue;
+ }
+ if (((_ref1 = properties[property]) != null ? _ref1.type : void 0) === 'int' && (value != null)) {
+ record[property] = parseInt(value, 10);
+ } else if (((_ref2 = properties[property]) != null ? _ref2.type : void 0) === 'date' && (value != null)) {
+ if (/^\d+$/.test(value)) {
+ value = parseInt(value, 10);
+ } else {
+ value = Date.parse(value);
+ }
+ if (options.milliseconds) {
+ record[property] = value;
+ } else if (options.seconds) {
+ record[property] = Math.round(value / 1000);
+ } else {
+ record[property] = new Date(value);
+ }
+ }
+ }
+ if (options.identifiers) {
+ records[i] = record[identifier];
+ }
+ } else if (typeof record === 'number' || typeof record === 'string') {
+ records[i] = parseInt(record);
+ }
+ }
+ if (isArray) {
+ return records;
+ } else {
+ return records[0];
+ }
+ };
+
+ /*
+
+ `validate(records, [options])`
+ ------------------------------
+ Validate the properties of one or more records. Return a validation
+ object or an array of validation objects depending on the provided
+ records arguments. Keys of a validation object are the name of the invalid
+ properties and their value is a string indicating the type of error.
+
+ `records` Record object or array of record objects.
+
+ `options` Options include:
+
+ * `throw` Throw errors on first invalid property instead of returning a validation object.
+ * `skip_required` Doesn't validate missing properties defined as `required`, usefull for partial update.
+ */
+
+
+ Schema.prototype.validate = function(records, options) {
+ var db, isArray, name, properties, property, record, validation, validations, x, _ref;
+ if (options == null) {
+ options = {};
+ }
+ _ref = this.data, db = _ref.db, name = _ref.name, properties = _ref.properties;
+ isArray = Array.isArray(records);
+ if (!isArray) {
+ records = [records];
+ }
+ validations = (function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = records.length; _i < _len; _i++) {
+ record = records[_i];
+ validation = {};
+ for (x in properties) {
+ property = properties[x];
+ if (!options.skip_required && property.required && !(record[property.name] != null)) {
+ if (options["throw"]) {
+ throw new Error("Required property " + property.name);
+ } else {
+ validation[property.name] = 'required';
+ }
+ } else if (property.type === 'email' && !isEmail(record[property.name])) {
+ if (options["throw"]) {
+ throw new Error("Invalid email " + record[property.name]);
+ } else {
+ validation[property.name] = 'invalid_email';
+ }
+ }
+ }
+ _results.push(validation);
+ }
+ return _results;
+ })();
+ if (isArray) {
+ return validations;
+ } else {
+ return validations[0];
+ }
+ };
+
+ return Schema;
+
+})();
View
79 lib/doc.js
@@ -0,0 +1,79 @@
+// Generated by CoffeeScript 1.4.0
+var convert_anchor, convert_code, date, each, fs, mecano;
+
+fs = require('fs');
+
+mecano = require('mecano');
+
+each = require('each');
+
+date = function() {
+ var d;
+ return d = (new Date).toISOString();
+};
+
+convert_anchor = function(text) {
+ var re_anchor;
+ re_anchor = /`([\w]+)\(/g;
+ return text.replace(re_anchor, function(str, code) {
+ return "<a name=\"" + code + "\"></a>\n`" + code + "(";
+ });
+};
+
+convert_code = function(text) {
+ var re_code;
+ re_code = /\n(\s{2}\s*?\w[\s\S]*?)\n(?!\s)/g;
+ return text.replace(re_code, function(str, code) {
+ code = code.split('\n').map(function(line) {
+ return line.substr(4);
+ }).join('\n');
+ return "\n```coffeescript\n" + code + "\n```\n";
+ });
+};
+
+each(['Client', 'Schema', 'Records']).parallel(true).on('item', function(next, file) {
+ var destination, source;
+ source = "" + __dirname + "/" + file + ".coffee";
+ destination = "" + __dirname + "/../docs/" + (file.toLowerCase()) + ".md";
+ return fs.readFile(source, 'ascii', function(err, content) {
+ var docs, match, re, re_title;
+ if (err) {
+ return console.error(err);
+ }
+ re = /###\n([\s\S]*?)\n( *)###/g;
+ re_title = /([\s\S]+)\n={2}=+([\s\S]*)/g;
+ match = re.exec(content);
+ match = re_title.exec(match[1]);
+ docs = "---\nlanguage: en\nlayout: page\ntitle: \"" + match[1] + "\"\ndate: " + (date()) + "\ncomments: false\nsharing: false\nfooter: false\nnavigation: ron\ngithub: https://github.com/wdavidw/node-ron\n---\n" + (convert_code(match[2]));
+ while (match = re.exec(content)) {
+ match[1] = match[1].split('\n').map(function(line) {
+ return line.substr(2);
+ }).join('\n');
+ docs += convert_code(convert_anchor(match[1]));
+ docs += '\n';
+ }
+ return fs.writeFile(destination, docs, next);
+ });
+}).on('both', function(err) {
+ var destination;
+ if (err) {
+ return console.error(err);
+ }
+ console.log('Documentation generated');
+ destination = process.argv[2];
+ if (!destination) {
+ return;
+ }
+ return each(['index', 'client', 'schema', 'records']).on('item', function(next, file) {
+ return mecano.copy({
+ source: "" + __dirname + "/../docs/" + file + ".md",
+ destination: destination,
+ force: true
+ }, next);
+ }).on('both', function(err) {
+ if (err) {
+ return console.error(err);
+ }
+ return console.log('Documentation published');
+ });
+});
View
0  lib/Client.coffee → src/Client.coffee
File renamed without changes
View
0  lib/Records.coffee → src/Records.coffee
File renamed without changes
View
0  lib/Schema.coffee → src/Schema.coffee
File renamed without changes
View
0  lib/doc.coffee → src/doc.coffee
File renamed without changes
Please sign in to comment.
Something went wrong with that request. Please try again.