Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Indentation as 2 spaces

  • Loading branch information...
commit 1817df692c090d8900661b066519d45122e8f502 1 parent 9aeefeb
@wdavidw authored
View
2  conf/redis.conf
@@ -3,4 +3,4 @@ appendonly yes
pidfile ./conf/redis.pid
port 6379
dbfilename dump.rdb
-dir ./conf
+dir ./conf
View
6 conf/test.coffee.sample
@@ -1,5 +1,5 @@
module.exports =
- port: 4501
- host: 'localhost'
- name: 'test'
+ port: 4501
+ host: 'localhost'
+ name: 'test'
View
48 docs/client.md
@@ -3,7 +3,7 @@ language: en
layout: page
title: "
Client connection"
-date: 2012-10-01T07:39:11.603Z
+date: 2012-11-15T21:16:28.466Z
comments: false
sharing: false
footer: false
@@ -17,28 +17,31 @@ and manipulation.
Internally, Ron use the [Redis client for Node.js](https://github.com/mranney/node_redis).
-<a name="ron"></a>`ron([options])` Client creation
+<a name="ron"></a>
+`ron([options])` Client creation
--------------------------------
-`options` Options properties include:
+`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).
+* `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:
```coffeescript
+
ron = require 'ron'
client = ron
- host: '127.0.0.1'
- port: 6379
+ host: '127.0.0.1'
+ port: 6379
```
-<a name="get"></a>`get(schema)` Records definition and access
+<a name="get"></a>
+`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
@@ -48,16 +51,18 @@ with the same name.
Define a record from a object:
```coffeescript
+
client.get
- name: 'users'
- properties:
- user_id: identifier: true
- username: unique: true
- email: index: true
+ name: 'users'
+ properties:
+ user_id: identifier: true
+ username: unique: true
+ email: index: true
```
Define a record from function calls:
```coffeescript
+
Users = client.get 'users'
Users.identifier 'user_id'
Users.unique 'username'
@@ -73,12 +78,13 @@ client.get 'username', temporal: true, properties: username: unique: true
```
-<a name="quit"></a>`quit(callback)` Quit
+<a name="quit"></a>
+`quit(callback)` Quit
---------------------
Destroy the redis connection.
-`callback` Received parameters are:
+`callback` Received parameters are:
-* `err` Error object if any.
-* `status` Status provided by the redis driver
+* `err` Error object if any.
+* `status` Status provided by the redis driver
View
16 docs/index.md
@@ -27,9 +27,9 @@ Usage
ron = require('ron');
// Client connection
client = ron({
- port: 6379
- host: '127.0.0.1'
- name: 'auth'
+ port: 6379
+ host: '127.0.0.1'
+ name: 'auth'
});
// Schema definition
Users = client.get('users');
@@ -39,17 +39,17 @@ Users.property('email', {index: true, type: 'email'});
Users.property('name', {});
// Record manipulation
Users.create(
- {username: 'ron', email: 'ron@domain.com'},
- function(err, user){
- console.log(err, user.id);
- }
+ {username: 'ron', email: 'ron@domain.com'},
+ function(err, user){
+ console.log(err, user.id);
+ }
)
```
The library provides
--------------------
-* Documented and tested API
+* Documented and tested API
* Records access with indexes and unique values
* Records are pure object, no extended class, no magic
View
205 docs/records.md
@@ -3,7 +3,7 @@ language: en
layout: page
title: "
Records access and manipulation"
-date: 2012-10-01T07:39:11.604Z
+date: 2012-11-15T21:16:28.466Z
comments: false
sharing: false
footer: false
@@ -43,42 +43,47 @@ 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.
-<a name="all"></a>`all(callback)`
+<a name="all"></a>
+`all(callback)`
---------------
Return all records. Similar to the find method with far less options
and a faster implementation.
-<a name="clear"></a>`clear(callback)`
+<a name="clear"></a>
+`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:
+`callback` Received parameters are:
-* `err` Error object if any.
-* `count` Number of removed records on success
+* `err` Error object if any.
+* `count` Number of removed records on success
-Usage:
+Usage:
```coffeescript
+
ron.get('users').clear (err, count) ->
- return console.error "Failed: #{err.message}" if err
- console.log "#{count} records removed"
+ return console.error "Failed: #{err.message}" if err
+ console.log "#{count} records removed"
```
-<a name="count"></a>`count(callback)`
+<a name="count"></a>
+`count(callback)`
-----------------
-Count the number of records present in the database.
+Count the number of records present in the database.
Counting all the records:
```coffeescript
Users.count, (err, count) ->
- console.log 'count users', count
+ console.log 'count users', count
```
-<a name="count"></a>`count(property, values, callback)`
+<a name="count"></a>
+`count(property, values, callback)`
----------------------------------
Count the number of one or more values for an indexed property.
@@ -86,15 +91,16 @@ Counting multiple values:
```coffeescript
Users.get 'users', properties:
- user_id: identifier: true
- job: index: true
+ 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]
+ console.log 'count globtrotter', counts[0]
+ console.log 'count icemaker', counts[1]
```
-<a name="create"></a>`create(records, [options], callback)`
+<a name="create"></a>
+`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
@@ -102,26 +108,27 @@ 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.
+`records` Record object or array of record objects.
-`options` Options properties include:
+`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.
+* `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:
+`callback` Called on success or failure. Received parameters are:
-* `err` Error object if any.
-* `records` Records with their newly created identifier.
+* `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.
-<a name="exists"></a>`exists(records, callback)`
+<a name="exists"></a>
+`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
@@ -129,40 +136,43 @@ 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.
+`records` Record object or array of record objects.
-`callback` Called on success or failure. Received parameters are:
+`callback` Called on success or failure. Received parameters are:
-* `err` Error object if any.
-* `identifier` Record identifiers or null values.
+* `err` Error object if any.
+* `identifier` Record identifiers or null values.
-<a name="get"></a>`get(records, [options], callback)`
+<a name="get"></a>
+`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:
+`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
+* `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:
+`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.
+* `err` Error object if the command failed.
+* `records` Object or array of object if command succeed. Objects are null if records are not found.
-<a name="id"></a>`id(records, callback)`
+<a name="id"></a>
+`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.
-<a name="identify"></a>`identify(records, [options], callback)`
+<a name="identify"></a>
+`identify(records, [options], callback)`
----------------------------------------
Extract record identifiers or set the identifier to null if its associated record could not be found.
@@ -170,71 +180,72 @@ 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.
+`records` Record object or array of record objects.
-`options` Options properties include:
+`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.
+* `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:
```coffeescript
Users.get 'users', properties:
- user_id: identifier: true
- username: unique: true
+ user_id: identifier: true
+ username: unique: true
Users.id [
- {username: 'username_1'}
- {username: 'username_2'}
+ {username: 'username_1'}
+ {username: 'username_2'}
], (err, ids) ->
- should.not.exist err
- console.log ids
+ should.not.exist err
+ console.log ids
```
Use the `object` option to return records instead of ids:
```coffeescript
Users.get 'users', properties:
- user_id: identifier: true
- username: unique: true
+ user_id: identifier: true
+ username: unique: true
Users.id [
- 1, {user_id: 2} ,{username: 'username_3'}
+ 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
+ should.not.exist err
+ ids = for user in users then user.user_id
+ console.log ids
```
-<a name="list"></a>`list([options], callback)`
+<a name="list"></a>
+`list([options], callback)`
---------------------------
List records with support for filtering and sorting.
-`options` Options properties include:
+`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.
+* `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:
+`callback` Called on success or failure. Received parameters are:
-* `err` Error object if any.
-* `records` Records fetched from Redis.
+* `err` Error object if any.
+* `records` Records fetched from Redis.
Using the `union` operation:
```coffeescript
Users.list
- where: group: ['admin', 'redis']
- operation: 'union'
- direction: 'desc'
+ where: group: ['admin', 'redis']
+ operation: 'union'
+ direction: 'desc'
, (err, users) ->
- console.log users
+ console.log users
```
An alternative syntax is to bypass the `where` option, the exemple above
@@ -242,50 +253,52 @@ could be rewritten as:
```coffeescript
Users.list
- group: ['admin', 'redis']
- operation: 'union'
- direction: 'desc'
+ group: ['admin', 'redis']
+ operation: 'union'
+ direction: 'desc'
, (err, users) ->
- console.log users
+ console.log users
```
-<a name="remove"></a>`remove(records, callback)`
+<a name="remove"></a>
+`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.
+`records` Record object or array of record objects.
-`callback` Called on success or failure. Received parameters are:
+`callback` Called on success or failure. Received parameters are:
-* `err` Error object if any.
-* `removed` Number of removed records.
+* `err` Error object if any.
+* `removed` Number of removed records.
Removing a single record:
```coffeescript
Users.remove id, (err, removed) ->
- console.log "#{removed} user removed"
+ console.log "#{removed} user removed"
```
-<a name="update"></a>`update(records, [options], callback)`
+<a name="update"></a>
+`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.
+`records` Record object or array of record objects.
-`options` Options properties include:
+`options` Options properties include:
-* `validate` Validate the records.
+* `validate` Validate the records.
-`callback` Called on success or failure. Received parameters are:
+`callback` Called on success or failure. Received parameters are:
-* `err` Error object if any.
-* `records` Records with their newly created identifier.
+* `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.
@@ -294,8 +307,8 @@ Updating a single record:
```coffeescript
Users.update
- username: 'my_username'
- age: 28
+ username: 'my_username'
+ age: 28
, (err, user) -> console.log user
```
View
87 docs/schema.md
@@ -3,7 +3,7 @@ language: en
layout: page
title: "
Schema definition"
-date: 2012-10-01T07:39:11.604Z
+date: 2012-11-15T21:16:28.467Z
comments: false
sharing: false
footer: false
@@ -16,28 +16,28 @@ 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
+`ron` Reference to the Ron instance
-`options` Schema definition. Options include:
+`options` Schema definition. Options include:
-* `name` Name of the schema.
-* `properties` Properties definition, an object or an array.
+* `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.
+* `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:
```coffeescript
ron.get 'users', properties:
- user_id: identifier: true
- username: unique: true
- password: true
+ user_id: identifier: true
+ username: unique: true
+ password: true
```
Define a schema with a declarative approach:
@@ -56,20 +56,23 @@ users = ron.get 'users'
users.list (err, users) -> console.log users
```
-<a name="hash"></a>`hash(key)`
+<a name="hash"></a>
+`hash(key)`
-------------
Utility function used when a redis key is created out of
uncontrolled character (like a string instead of an int).
-<a name="identifier"></a>`identifier(property)`
+<a name="identifier"></a>
+`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.
-<a name="index"></a>`index([property])`
+<a name="index"></a>
+`index([property])`
-------------------
Define a property as indexable or return all index properties. An
indexed property allow records access by its property value. For example,
@@ -79,17 +82,19 @@ records match one or multiple values.
Calling this function without any argument will return an array with all the
indexed properties.
-Example:
+Example:
```coffeescript
+
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}"
+ console.log 'This user has the following accounts:'
+ for user in user
+ console.log "- #{user.username}"
```
-<a name="property"></a>`property(property, [schema])`
+<a name="property"></a>
+`property(property, [schema])`
------------------------------
Define a new property or overwrite the definition of an
existing property. If no schema is provide, return the
@@ -111,7 +116,8 @@ User.property 'name', {}
```
-<a name="name"></a>`name()`
+<a name="name"></a>
+`name()`
--------
Return the schema name of the current instance.
@@ -122,7 +128,8 @@ console.log Users.name() is 'users'
```
-<a name="serialize"></a>`serialize(records)`
+<a name="serialize"></a>
+`serialize(records)`
--------------------
Cast record values before their insertion into Redis.
@@ -130,7 +137,8 @@ Take a record or an array of records and update values with correct
property types.
-<a name="temporal"></a>`temporal([options])`
+<a name="temporal"></a>
+`temporal([options])`
---------------------
Define or retrieve temporal definition. Marking a schema as
temporal will transparently add two new date properties, the
@@ -138,48 +146,51 @@ date when the record was created (by default "cdate"), and the date
when the record was last updated (by default "mdate").
-<a name="unique"></a>`unique([property])`
+<a name="unique"></a>
+`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:
+Example:
```coffeescript
User.unique 'username'
User.get { username: 'me' }, (err, user) ->
- console.log "This is #{user.username}"
+ console.log "This is #{user.username}"
```
-<a name="unserialize"></a>`unserialize(records, [options])`
+<a name="unserialize"></a>
+`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:
+`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.
+* `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.
-<a name="validate"></a>`validate(records, [options])`
+<a name="validate"></a>
+`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.
+`records` Record object or array of record objects.
-`options` Options include:
+`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.
+* `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.
View
2  index.js
@@ -4,7 +4,7 @@ require('coffee-script');
var Client = require('./lib/Client');
module.exports = function(options){
- return new Client(options);
+ return new Client(options);
};
module.exports.Client = Client;
View
191 lib/Client.coffee
@@ -16,97 +16,100 @@ Internally, Ron use the [Redis client for Node.js](https://github.com/mranney/no
###
module.exports = class Client
- ###
-
- `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
-
- ###
- constructor: (options = {}) ->
- @options = options
- @name = options.name or 'ron'
- @schemas = {}
- @records = {}
- if @options.redis
- @redis = @options.redis
- else
- @redis = redis.createClient options.port ? 6379, options.host ? '127.0.0.1'
- @redis.auth options.password if options.password?
- @redis.select options.database if 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
-
- ###
- get: (schema) ->
- create = true
- if arguments.length > 1
- if typeof arguments[0] is 'string'
- then schema = name: arguments[0]
- else schema = arguments[0]
- for i in [1 ... arguments.length]
- for k, v of arguments[i]
- schema[k] = v
- else if typeof schema is 'string'
- schema = {name: schema}
- create = false if @records[schema.name]?
- @records[schema.name] = new Records @, schema if create
- @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
-
- ###
- quit: (callback) ->
- @redis.quit (err, status) ->
- return unless callback
- return callback err if err
- callback null, status if callback
+ ###
+
+ `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
+
+ ###
+ constructor: (options = {}) ->
+ @options = options
+ @name = options.name or 'ron'
+ @schemas = {}
+ @records = {}
+ if @options.redis
+ @redis = @options.redis
+ else
+ @redis = redis.createClient options.port ? 6379, options.host ? '127.0.0.1'
+ @redis.auth options.password if options.password?
+ @redis.select options.database if 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
+
+ ###
+ get: (schema) ->
+ create = true
+ if arguments.length > 1
+ if typeof arguments[0] is 'string'
+ then schema = name: arguments[0]
+ else schema = arguments[0]
+ for i in [1 ... arguments.length]
+ for k, v of arguments[i]
+ schema[k] = v
+ else if typeof schema is 'string'
+ schema = {name: schema}
+ create = false if @records[schema.name]?
+ @records[schema.name] = new Records @, schema if create
+ @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
+
+ ###
+ quit: (callback) ->
+ @redis.quit (err, status) ->
+ return unless callback
+ return callback err if err
+ callback null, status if callback
View
1,411 lib/Records.coffee
@@ -40,714 +40,715 @@ associated to the indexed property and values are the record identifiers.
###
module.exports = class Records extends Schema
- constructor: (ron, schema) ->
- @redis = ron.redis
- super ron, schema
- ###
-
- `all(callback)`
- ---------------
- Return all records. Similar to the find method with far less options
- and a faster implementation.
-
- ###
- all: (callback) ->
- {redis} = @
- {db, name, identifier} = @data
- redis.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) =>
- multi = redis.multi()
- for recordId in recordIds
- multi.hgetall "#{db}:#{name}:#{recordId}"
- multi.exec (err, records) =>
- return callback err if err
- @unserialize records
- 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"
-
- ###
- clear: (callback) ->
- {redis, hash} = @
- {db, name, identifier, index, unique} = @data
- cmds = []
- count = 0
+ constructor: (ron, schema) ->
+ @redis = ron.redis
+ super ron, schema
+ ###
+
+ `all(callback)`
+ ---------------
+ Return all records. Similar to the find method with far less options
+ and a faster implementation.
+
+ ###
+ all: (callback) ->
+ {redis} = @
+ {db, name, identifier} = @data
+ redis.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) =>
+ multi = redis.multi()
+ for recordId in recordIds
+ multi.hgetall "#{db}:#{name}:#{recordId}"
+ multi.exec (err, records) =>
+ return callback err if err
+ @unserialize records
+ 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"
+
+ ###
+ clear: (callback) ->
+ {redis, hash} = @
+ {db, name, identifier, index, unique} = @data
+ cmds = []
+ count = 0
+ multi = redis.multi()
+ # Grab index values for later removal
+ indexSort = []
+ indexProperties = Object.keys(index)
+ if indexProperties.length
+ indexSort.push "#{db}:#{name}_#{identifier}"
+ for property in indexProperties
+ indexSort.push 'get'
+ indexSort.push "#{db}:#{name}:*->#{property}"
+ # Delete null index
+ cmds.push ['del', "#{db}:#{name}_#{property}:null"]
+ indexSort.push (err, values) ->
+ if values.length
+ for i in [0 ... values.length] by indexProperties.length
+ for property, j in indexProperties
+ value = hash values[i + j]
+ cmds.push ['del', "#{db}:#{name}_#{property}:#{value}"]
+ multi.sort indexSort...
+ # Grab record identifiers
+ multi.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) ->
+ return callback err if err
+ # Return count in final callback
+ # console.log 'recordIds', err, recordIds
+ recordIds ?= []
+ count = recordIds.length
+ # delete objects
+ for recordId in recordIds
+ cmds.push ['del', "#{db}:#{name}:#{recordId}"]
+ # Incremental counter
+ cmds.push ['del', "#{db}:#{name}_incr"]
+ # Identifier index
+ cmds.push ['del', "#{db}:#{name}_#{identifier}"]
+ # Unique indexes
+ for property of unique
+ cmds.push ['del', "#{db}:#{name}_#{property}"]
+ # Index of values
+ for property of index
+ cmds.push ['del', "#{db}:#{name}_#{property}"]
+ multi.exec (err, results) ->
+ return callback err if err
+ multi = redis.multi cmds
+ multi.exec (err, results) ->
+ return callback err if err
+ 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]
+
+ ###
+ count: (callback) ->
+ {redis} = @
+ {db, name, identifier, index} = @data
+ if arguments.length is 3
+ property = callback
+ values = arguments[1]
+ callback = arguments[2]
+ return callback new Error "Property is not indexed" unless index[property]
+ isArray = Array.isArray values
+ values = [values] unless isArray
+ multi = redis.multi()
+ for value, i in values
+ value = @hash value
+ multi.scard "#{db}:#{name}_#{property}:#{value}"
+ multi.exec (err, counts) ->
+ return callback err if err
+ callback null, if isArray then counts else counts[0]
+ else
+ @redis.scard "#{db}:#{name}_#{identifier}", (err, count) ->
+ return callback err if err
+ 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.
+
+ ###
+ create: (records, options, callback) ->
+ if arguments.length is 2
+ callback = options
+ options = {}
+ {redis, hash} = @
+ {db, name, temporal, properties, identifier, index, unique} = @data
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ # Validate records
+ if options.validate
+ try @validate records, throw: true
+ catch e then return callback e, (if isArray then records else records[0])
+ # Persist
+ @exists records, (err, recordIds) =>
+ return callback err if err
+ for recordId in recordIds
+ return callback new Error "Record #{recordId} already exists" if recordId?
+ multi = redis.multi()
+ # Get current date once if schema is temporal
+ date = new Date Date.now() if temporal?
+ # Generate new identifiers
+ @id records, (err, records) =>
multi = redis.multi()
- # Grab index values for later removal
- indexSort = []
- indexProperties = Object.keys(index)
- if indexProperties.length
- indexSort.push "#{db}:#{name}_#{identifier}"
- for property in indexProperties
- indexSort.push 'get'
- indexSort.push "#{db}:#{name}:*->#{property}"
- # Delete null index
- cmds.push ['del', "#{db}:#{name}_#{property}:null"]
- indexSort.push (err, values) ->
- if values.length
- for i in [0 ... values.length] by indexProperties.length
- for property, j in indexProperties
- value = hash values[i + j]
- cmds.push ['del', "#{db}:#{name}_#{property}:#{value}"]
- multi.sort indexSort...
- # Grab record identifiers
- multi.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) ->
- return callback err if err
- # Return count in final callback
- # console.log 'recordIds', err, recordIds
- recordIds ?= []
- count = recordIds.length
- # delete objects
- for recordId in recordIds
- cmds.push ['del', "#{db}:#{name}:#{recordId}"]
- # Incremental counter
- cmds.push ['del', "#{db}:#{name}_incr"]
- # Identifier index
- cmds.push ['del', "#{db}:#{name}_#{identifier}"]
- # Unique indexes
- for property of unique
- cmds.push ['del', "#{db}:#{name}_#{property}"]
- # Index of values
- for property of index
- cmds.push ['del', "#{db}:#{name}_#{property}"]
- multi.exec (err, results) ->
- return callback err if err
- multi = redis.multi cmds
- multi.exec (err, results) ->
- return callback err if err
- 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]
-
- ###
- count: (callback) ->
- {redis} = @
- {db, name, identifier, index} = @data
- if arguments.length is 3
- property = callback
- values = arguments[1]
- callback = arguments[2]
- return callback new Error "Property is not indexed" unless index[property]
- isArray = Array.isArray values
- values = [values] unless isArray
- multi = redis.multi()
- for value, i in values
- value = @hash value
- multi.scard "#{db}:#{name}_#{property}:#{value}"
- multi.exec (err, counts) ->
- return callback err if err
- callback null, if isArray then counts else counts[0]
- else
- @redis.scard "#{db}:#{name}_#{identifier}", (err, count) ->
- return callback err if err
- 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.
-
- ###
- create: (records, options, callback) ->
- if arguments.length is 2
- callback = options
- options = {}
- {redis, hash} = @
- {db, name, temporal, properties, identifier, index, unique} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- # Validate records
- if options.validate
- try @validate records, throw: true
- catch e then return callback e, (if isArray then records else records[0])
- # Persist
- @exists records, (err, recordIds) =>
- return callback err if err
- for recordId in recordIds
- return callback new Error "Record #{recordId} already exists" if recordId?
- multi = redis.multi()
- # Get current date once if schema is temporal
- date = new Date Date.now() if temporal?
- # Generate new identifiers
- @id records, (err, records) =>
- multi = redis.multi()
- for record, i in records
- # Enrich the record with a creation date
- record[temporal.creation] = date if temporal?.creation? and not record[temporal.creation]?
- # Enrich the record with a creation date
- record[temporal.modification] = date if temporal?.modification? and not record[temporal.modification]?
- # Register new identifier
- multi.sadd "#{db}:#{name}_#{identifier}", record[identifier]
- # Deal with Unique
- for property of unique
- multi.hset "#{db}:#{name}_#{property}", record[property], record[identifier] if record[property]
- # Deal with Index
- for property of index
- value = record[property]
- value = hash value
- multi.sadd "#{db}:#{name}_#{property}:#{value}", record[identifier]
- #multi.zadd "#{s.db}:#{s.name}_#{property}", 0, record[property]
- # Store the record
- r = {}
- for property, value of record
- # Insert only defined properties
- continue unless properties[property]
- # Filter null values
- r[property] = value if value?
- @serialize r
- multi.hmset "#{db}:#{name}:#{record[identifier]}", r
- multi.exec (err, results) =>
- return callback err if err
- for result in results
- return callback new Error 'Corrupted user database ' if result[0] is not "0"
- @unserialize records, options
- callback null, if isArray then records else 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.
-
- ###
- exists: (records, callback) ->
- {redis} = @
- {db, name, identifier, unique} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- multi = redis.multi()
- for record in records
- if typeof record is 'object'
- if record[identifier]?
- recordId = record[identifier]
- multi.hget "#{db}:#{name}:#{recordId}", identifier
- else
- for property of unique
- if record[property]?
- multi.hget "#{db}:#{name}_#{property}", record[property]
- else
- multi.hget "#{db}:#{name}:#{record}", identifier
- multi.exec (err, recordIds) =>
- return callback err if err
- @unserialize recordIds
- callback null, if isArray then recordIds else 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.
-
- ###
- get: (records, options, callback) ->
- if arguments.length is 2
- callback = options
- options = {}
- if Array.isArray options
- options = {properties: options}
- {redis} = @
- {db, name, identifier} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- # Quick exit for accept_null
- if options.accept_null? and not records.some((record) -> record isnt null)
- return callback null, if isArray then records else records[0]
- # Retrieve records identifiers
- @identify records, {object: true, accept_null: options.accept_null?}, (err, records) =>
- return callback err if err
- cmds = []
- records.forEach (record, i) ->
- # An error would have been thrown by id if record was null and accept_null wasn't provided
- return unless record?
- recordId = record[identifier]
- if recordId is null
- records[i] = null
- else if options.properties?.length
- options.properties.forEach (property) ->
- unless options.force or record[property]
- cmds.push ['hget', "#{db}:#{name}:#{recordId}", property, (err, value) ->
- record[property] = value
- ]
- else
- cmds.push ['hgetall', "#{db}:#{name}:#{recordId}", (err, values) ->
- for property, value of values
- record[property] = value
- ]
- # Check if the record really exists
- cmds.push ['exists', "#{db}:#{name}:#{recordId}", (err, exists) ->
- records[i] = null unless exists
- ]
- # No need to go further
- if cmds.length is 0
- return callback null, if isArray then records else records[0]
- multi = redis.multi cmds
- multi.exec (err, values) =>
- return callback err if err
- @unserialize records
- if options.object
- recordsByIds = {}
- for record in records
- recordsByIds[record[identifier]] = record
- callback null, recordsByIds
- else
- callback null, if isArray then records else 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.
-
- ###
- id: (records, callback) ->
- {redis} = @
- {db, name, identifier, unique} = @data
- if typeof records is 'number'
- incr = records
- isArray = true
- records = for i in [0...records] then null
- else
- isArray = Array.isArray records
- records = [records] unless isArray
- incr = 0
- for record in records then incr++ unless record[identifier]
- redis.incrby "#{db}:#{name}_incr", incr, (err, recordId) =>
- recordId = recordId - incr
- return callback err if err
- for record, i in records
- records[i] = record = {} unless record
- recordId++ unless record[identifier]
- # Enrich the record with its identifier
- record[identifier] = recordId unless record[identifier]
- callback null, if isArray then records else 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
-
- ###
- identify: (records, options, callback) ->
- if arguments.length is 2
- callback = options
- options = {}
- {redis} = @
- {db, name, identifier, unique} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- cmds = []
- err = null
for record, i in records
- if typeof record is 'object'
- unless record?
- # Check if we allow records to be null
- unless options.accept_null
- return callback new Error 'Null record'
- else if record[identifier]?
- # It's perfect, no need to hit redis
- else
- withUnique = false
- for property of unique
- if record[property]?
- withUnique = true
- cmds.push ['hget', "#{db}:#{name}_#{property}", record[property], ((record) -> (err, recordId) ->
- record[identifier] = recordId
- )(record)]
- # Error if no identifier and no unique value provided
- return callback new Error 'Invalid record, got ' + (JSON.stringify record) unless withUnique
- else if typeof record is 'number' or typeof record is 'string'
- records[i] = {}
- records[i][identifier] = record
- else
- return callback new Error 'Invalid id, got ' + (JSON.stringify record)
- finalize = ->
- unless options.object
- records = for record in records
- if record? then record[identifier] else record
- callback null, if isArray then records else records[0]
- # No need to hit redis if there is no command
- return finalize() if cmds.length is 0
- # Run the commands
- multi = redis.multi cmds
- multi.exec (err, results) =>
- return callback err if err
- @unserialize records
- 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
-
- ###
- list: (options, callback) ->
- if typeof options is 'function'
- callback = options
- options = {}
- {redis, hash} = @
- {db, name, identifier, index} = @data
- options.properties = options.properties or Object.keys @data.properties
- options.properties = [identifier] if options.identifiers
- args = []
- multi = @redis.multi()
- # Index
- options.where = {} unless options.where?
- where = []
- for property, value of options
- if index[property]
- if Array.isArray value
- for v in value
- where.push [property, v]
- else
- where.push [property, value]
- options.where = if Object.keys(options.where).length then options.where else false
- if where.length is 1
- [property, value] = where[0]
+ # Enrich the record with a creation date
+ record[temporal.creation] = date if temporal?.creation? and not record[temporal.creation]?
+ # Enrich the record with a creation date
+ record[temporal.modification] = date if temporal?.modification? and not record[temporal.modification]?
+ # Register new identifier
+ multi.sadd "#{db}:#{name}_#{identifier}", record[identifier]
+ # Deal with Unique
+ for property of unique
+ multi.hset "#{db}:#{name}_#{property}", record[property], record[identifier] if record[property]
+ # Deal with Index
+ for property of index
+ value = record[property]
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 filter in where
- [property, value] = filter
- value = hash value
- keys.push "#{db}:#{name}_#{property}:#{value}"
- operation = options.operation ? 'union'
- multi["s#{operation}store"] keys...
+ multi.sadd "#{db}:#{name}_#{property}:#{value}", record[identifier]
+ #multi.zadd "#{s.db}:#{s.name}_#{property}", 0, record[property]
+ # Store the record
+ r = {}
+ for property, value of record
+ # Insert only defined properties
+ continue unless properties[property]
+ # Filter null values
+ r[property] = value if value?
+ @serialize r
+ multi.hmset "#{db}:#{name}:#{record[identifier]}", r
+ multi.exec (err, results) =>
+ return callback err if err
+ for result in results
+ return callback new Error 'Corrupted user database ' if result[0] is not "0"
+ @unserialize records, options
+ callback null, if isArray then records else 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.
+
+ ###
+ exists: (records, callback) ->
+ {redis} = @
+ {db, name, identifier, unique} = @data
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ multi = redis.multi()
+ for record in records
+ if typeof record is 'object'
+ if record[identifier]?
+ recordId = record[identifier]
+ multi.hget "#{db}:#{name}:#{recordId}", identifier
else
- args.push "#{db}:#{name}_#{identifier}"
- # Sorting by one property
- if options.sort?
- args.push 'by'
- args.push "#{db}:#{name}:*->" + options.sort
- # Properties to return
- for property in options.properties
- args.push 'get'
- args.push "#{db}:#{name}:*->" + property
- # Sorting property is a string
- args.push 'alpha'
- # Sorting direction
- args.push options.direction ? 'asc'
- # Callback
- args.push (err, values) =>
- return callback err if err
- return callback null, [] unless values.length
- result = for i in [0 ... values.length] by options.properties.length
- record = {}
- for property, j in options.properties
- record[property] = values[i + j]
- @unserialize record, options
- callback null, result
- # Run command
- multi.sort args...
- multi.del tempkey if tempkey
- 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"
-
- ###
- remove: (records, callback) ->
- {redis, hash} = @
- {db, name, identifier, index, unique} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- removed = 0
- @get records, [].concat(Object.keys(unique), Object.keys(index)), (err, records) ->
- return callback err if err
- multi = redis.multi()
- for record in records
- # Bypass null records, not much to do with it
- continue if record is null
- do (record) ->
- # delete objects
- recordId = record[identifier]
- multi.del "#{db}:#{name}:#{recordId}", (err) ->
- removed++
- # delete indexes
- multi.srem "#{db}:#{name}_#{identifier}", recordId
- for property of unique
- multi.hdel "#{db}:#{name}_#{property}", record[property]
- for property of index
- value = hash record[property]
- multi.srem "#{db}:#{name}_#{property}:#{value}", recordId, (err, count) ->
- console.warn('Missing indexed property') if count isnt 1
- multi.exec (err, results) ->
- return callback err if err
- 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
-
- ###
- update: (records, options, callback) ->
- if arguments.length is 2
- callback = options
- options = {}
- {redis, hash} = @
- {db, name, temporal, properties, identifier, unique, index} = @data
- isArray = Array.isArray records
- records = [records] unless isArray
- # Validate records
- if options.validate
- try @validate records, {throw: true, skip_required: true}
- catch e then return callback e, (if isArray then records else records[0])
- # 1. Get values of indexed properties
- # 2. If indexed properties has changed
- # 2.1 Make sure the new property is not assigned to another record
- # 2.2 Erase old index & Create new index
- # 3. Save the record
- @identify records, {object: true}, (err, records) =>
+ for property of unique
+ if record[property]?
+ multi.hget "#{db}:#{name}_#{property}", record[property]
+ else
+ multi.hget "#{db}:#{name}:#{record}", identifier
+ multi.exec (err, recordIds) =>
+ return callback err if err
+ @unserialize recordIds
+ callback null, if isArray then recordIds else 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.
+
+ ###
+ get: (records, options, callback) ->
+ if arguments.length is 2
+ callback = options
+ options = {}
+ if Array.isArray options
+ options = {properties: options}
+ {redis} = @
+ {db, name, identifier} = @data
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ # Quick exit for accept_null
+ if options.accept_null? and not records.some((record) -> record isnt null)
+ return callback null, if isArray then records else records[0]
+ # Retrieve records identifiers
+ @identify records, {object: true, accept_null: options.accept_null?}, (err, records) =>
+ return callback err if err
+ cmds = []
+ records.forEach (record, i) ->
+ # An error would have been thrown by id if record was null and accept_null wasn't provided
+ return unless record?
+ recordId = record[identifier]
+ if recordId is null
+ records[i] = null
+ else if options.properties?.length
+ options.properties.forEach (property) ->
+ unless options.force or record[property]
+ cmds.push ['hget', "#{db}:#{name}:#{recordId}", property, (err, value) ->
+ record[property] = value
+ ]
+ else
+ cmds.push ['hgetall', "#{db}:#{name}:#{recordId}", (err, values) ->
+ for property, value of values
+ record[property] = value
+ ]
+ # Check if the record really exists
+ cmds.push ['exists', "#{db}:#{name}:#{recordId}", (err, exists) ->
+ records[i] = null unless exists
+ ]
+ # No need to go further
+ if cmds.length is 0
+ return callback null, if isArray then records else records[0]
+ multi = redis.multi cmds
+ multi.exec (err, values) =>
+ return callback err if err
+ @unserialize records
+ if options.object
+ recordsByIds = {}
+ for record in records
+ recordsByIds[record[identifier]] = record
+ callback null, recordsByIds
+ else
+ callback null, if isArray then records else 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.
+
+ ###
+ id: (records, callback) ->
+ {redis} = @
+ {db, name, identifier, unique} = @data
+ if typeof records is 'number'
+ incr = records
+ isArray = true
+ records = for i in [0...records] then null
+ else
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ incr = 0
+ for record in records then incr++ unless record[identifier]
+ redis.incrby "#{db}:#{name}_incr", incr, (err, recordId) =>
+ recordId = recordId - incr
+ return callback err if err
+ for record, i in records
+ records[i] = record = {} unless record
+ recordId++ unless record[identifier]
+ # Enrich the record with its identifier
+ record[identifier] = recordId unless record[identifier]
+ callback null, if isArray then records else 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
+
+ ###
+ identify: (records, options, callback) ->
+ if arguments.length is 2
+ callback = options
+ options = {}
+ {redis} = @
+ {db, name, identifier, unique} = @data
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ cmds = []
+ err = null
+ for record, i in records
+ if typeof record is 'object'
+ unless record?
+ # Check if we allow records to be null
+ unless options.accept_null
+ return callback new Error 'Null record'
+ else if record[identifier]?
+ # It's perfect, no need to hit redis
+ else
+ withUnique = false
+ for property of unique
+ if record[property]?
+ withUnique = true
+ cmds.push ['hget', "#{db}:#{name}_#{property}", record[property], ((record) -> (err, recordId) ->
+ record[identifier] = recordId
+ )(record)]
+ # Error if no identifier and no unique value provided
+ return callback new Error 'Invalid record, got ' + (JSON.stringify record) unless withUnique
+ else if typeof record is 'number' or typeof record is 'string'
+ records[i] = {}
+ records[i][identifier] = record
+ else
+ return callback new Error 'Invalid id, got ' + (JSON.stringify record)
+ finalize = ->
+ unless options.object
+ records = for record in records
+ if record? then record[identifier] else record
+ callback null, if isArray then records else records[0]
+ # No need to hit redis if there is no command
+ return finalize() if cmds.length is 0
+ # Run the commands
+ multi = redis.multi cmds
+ multi.exec (err, results) =>
+ return callback err if err
+ @unserialize records
+ 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
+
+ ###
+ list: (options, callback) ->
+ if typeof options is 'function'
+ callback = options
+ options = {}
+ {redis, hash} = @
+ {db, name, identifier, index} = @data
+ options.properties = options.properties or Object.keys @data.properties
+ options.properties = [identifier] if options.identifiers
+ args = []
+ multi = @redis.multi()
+ # Index
+ options.where = {} unless options.where?
+ where = []
+ for property, value of options
+ if index[property]
+ if Array.isArray value
+ for v in value
+ where.push [property, v]
+ else
+ where.push [property, value]
+ options.where = if Object.keys(options.where).length then options.where else false
+ if where.length is 1
+ [property, value] = where[0]
+ 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 filter in where
+ [property, value] = filter
+ value = hash value
+ keys.push "#{db}:#{name}_#{property}:#{value}"
+ operation = options.operation ? 'union'
+ multi["s#{operation}store"] keys...
+ else
+ args.push "#{db}:#{name}_#{identifier}"
+ # Sorting by one property
+ if options.sort?
+ args.push 'by'
+ args.push "#{db}:#{name}:*->" + options.sort
+ # Properties to return
+ for property in options.properties
+ args.push 'get'
+ args.push "#{db}:#{name}:*->" + property
+ # Sorting property is a string
+ args.push 'alpha'
+ # Sorting direction
+ args.push options.direction ? 'asc'
+ # Callback
+ args.push (err, values) =>
+ return callback err if err
+ return callback null, [] unless values.length
+ result = for i in [0 ... values.length] by options.properties.length
+ record = {}
+ for property, j in options.properties
+ record[property] = values[i + j]
+ @unserialize record, options
+ callback null, result
+ # Run command
+ multi.sort args...
+ multi.del tempkey if tempkey
+ 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"
+
+ ###
+ remove: (records, callback) ->
+ {redis, hash} = @
+ {db, name, identifier, index, unique} = @data
+ isArray = Array.isArray records
+ records = [records] unless isArray
+ removed = 0
+ @get records, [].concat(Object.keys(unique), Object.keys(index)), (err, records) ->
+ return callback err if err
+ multi = redis.multi()
+ for record in records
+ # Bypass null records, not much to do with it
+ continue if record is null
+ do (record) ->
+ # delete objects
+ recordId = record[identifier]
+ multi.del "#{db}:#{name}:#{recordId}", (err) ->
+ removed++
+ # delete indexes
+ multi.srem "#{db}:#{name}_#{identifier}", recordId
+ for property of unique