├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── conf
├── redis.conf
└── test.coffee.sample
├── doc
├── client.md
├── coverage.html
├── index.md
├── records.md
└── schema.md
├── lib
├── Client.js
├── Records.js
├── Schema.js
├── doc.js
└── index.js
├── package.json
├── samples
└── create.js
├── src
├── Client.coffee.md
├── Records.coffee.md
├── Schema.coffee.md
├── doc.coffee
└── index.coffee
└── test
├── all.coffee
├── clear.coffee
├── client_get.coffee
├── count.coffee
├── create.coffee
├── exists.coffee
├── get.coffee
├── id.coffee
├── identify.coffee
├── list.coffee
├── mocha.opts
├── remove.coffee
├── schema_hash.coffee
├── schema_temporal.coffee
├── type.coffee
├── type_date.coffee
├── type_email.coffee
└── update.coffee
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | /conf/test.coffee
3 | /conf/dump.rdb
4 | /node_modules/*
5 | !.travis.yml
6 | !.gitignore
7 | conf/appendonly.aof
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | language: node_js
3 | sudo: required
4 | services:
5 | - docker
6 | node_js:
7 | - "4"
8 | - "5"
9 | - "6"
10 | before_install:
11 | - "docker run --name ron -p 6379:6379 -d redis"
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008-2012, SARL Adaltas.
2 | All rights reserved.
3 |
4 | Redistribution and use of this software in source and binary forms, with or
5 | without modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | - Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | - Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | - Neither the name of SARL Adaltas nor the names of its contributors may be
16 | used to endorse or promote products derived from this software without
17 | specific prior written permission of SARL Adaltas.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | REPORTER = dot
3 |
4 | doc: build
5 | @./node_modules/.bin/coffee src/doc $(RON_DOC)
6 |
7 | .PHONY: test
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node[](http://travis-ci.org/adaltas/node-ron)
2 |
3 | Redis ORM for NodeJs
4 | ====================
5 |
6 | Installation
7 | ------------
8 |
9 | ```bash
10 | npm install ron
11 | ```
12 |
13 | Usage
14 | -----
15 |
16 | ```javascript
17 | ron = require('ron');
18 | // Client connection
19 | client = ron({
20 | port: 6379,
21 | host: '127.0.0.1',
22 | name: 'auth'
23 | });
24 | // Schema definition
25 | Users = client.get('users');
26 | Users.property('id', {identifier: true});
27 | Users.property('username', {unique: true});
28 | Users.property('email', {index: true, type: 'email'});
29 | Users.property('name', {});
30 | // Record manipulation
31 | Users.create(
32 | {username: 'ron', email: 'ron@domain.com'},
33 | function(err, user){
34 | console.log(err, user.id);
35 | }
36 | );
37 | ```
38 |
39 | The library provide
40 | -------------------
41 |
42 | * Documented and tested API
43 | * Records access with indexes and unique values
44 | * Records are pure object, no state, no magic
45 |
46 | Client API
47 | ----------
48 |
49 | * [Client::constructor](http://www.adaltas.com/projects/node-ron/client.html#ron)
50 | * [Client::get](http://www.adaltas.com/projects/node-ron/client.html#get)
51 | * [Client::quit](http://www.adaltas.com/projects/node-ron/client.html#quit)
52 |
53 | Schema API
54 | ----------
55 |
56 | * [Records::hash](http://www.adaltas.com/projects/node-ron/schema.html#hash)
57 | * [Records::identifier](http://www.adaltas.com/projects/node-ron/schema.html#identifier)
58 | * [Records::index](http://www.adaltas.com/projects/node-ron/schema.html#index)
59 | * [Records::property](schema.html#property)
60 | * [Records::name](http://www.adaltas.com/projects/node-ron/schema.html#name)
61 | * [Records::serialize](http://www.adaltas.com/projects/node-ron/schema.html#serialize)
62 | * [Records::temporal](http://www.adaltas.com/projects/node-ron/schema.html#temporal)
63 | * [Records::unique](http://www.adaltas.com/projects/node-ron/schema.html#unique)
64 | * [Records::unserialize](http://www.adaltas.com/projects/node-ron/schema.html#unserialize)
65 | * [Records::validate](http://www.adaltas.com/projects/node-ron/schema.html#validate)
66 |
67 | Records API
68 | -----------
69 |
70 | * [Records::all](http://www.adaltas.com/projects/node-ron/records.html#all)
71 | * [Records::clear](http://www.adaltas.com/projects/node-ron/records.html#clear)
72 | * [Records::count](http://www.adaltas.com/projects/node-ron/records.html#count)
73 | * [Records::create](http://www.adaltas.com/projects/node-ron/records.html#create)
74 | * [Records::exists](http://www.adaltas.com/projects/node-ron/records.html#exists)
75 | * [Records::get](http://www.adaltas.com/projects/node-ron/records.html#get)
76 | * [Records::id](http://www.adaltas.com/projects/node-ron/records.html#id)
77 | * [Records::list](http://www.adaltas.com/projects/node-ron/records.html#list)
78 | * [Records::remove](http://www.adaltas.com/projects/node-ron/records.html#remove)
79 | * [Records::update](http://www.adaltas.com/projects/node-ron/records.html#update)
80 |
81 | Run tests
82 | ---------
83 |
84 | Run the tests with mocha:
85 | ```bash
86 | npm run redis_start
87 | npm test
88 | npm run redis_stop
89 | ```
90 |
91 | Note, the command above use a Docker container. You can use you're own Redis server by only running `npm test` after modifying the configuration file located in "conf/test.coffee".
92 |
93 | If Redis is installed, start a redis server on the default port:
94 | `redis-server ./conf/redis.conf`
95 |
96 | If Docker is installed, start a container:
97 | `docker run --name ron -p 6379:6379 -d redis redis-server --appendonly yes`
98 |
--------------------------------------------------------------------------------
/conf/redis.conf:
--------------------------------------------------------------------------------
1 | daemonize yes
2 | appendonly yes
3 | pidfile ./conf/redis.pid
4 | port 6379
5 | dbfilename dump.rdb
6 | dir ./conf
7 |
--------------------------------------------------------------------------------
/conf/test.coffee.sample:
--------------------------------------------------------------------------------
1 |
2 | module.exports =
3 | port: 6379
4 | host: 'localhost'
5 | name: 'test'
6 |
--------------------------------------------------------------------------------
/doc/client.md:
--------------------------------------------------------------------------------
1 | ---
2 | language: en
3 | layout: page
4 | title: "
5 | Client connection"
6 | date: 2012-11-15T21:39:55.922Z
7 | comments: false
8 | sharing: false
9 | footer: false
10 | navigation: ron
11 | github: https://github.com/wdavidw/node-ron
12 | ---
13 |
14 |
15 | The client wraps a redis connection and provides access to records definition
16 | and manipulation.
17 |
18 | Internally, Ron use the [Redis client for Node.js](https://github.com/mranney/node_redis).
19 |
20 |
21 | `ron([options])` Client creation
22 | --------------------------------
23 |
24 | `options` Options properties include:
25 |
26 | * `name` A namespace for the application, all keys with be prefixed with "#{name}:". Default to "ron"
27 | * `redis` Provide an existing instance in case you don't want a new one to be created.
28 | * `host` Redis hostname.
29 | * `port` Redis port.
30 | * `password` Redis password.
31 | * `database` Redis database (an integer).
32 |
33 | Basic example:
34 | ```coffeescript
35 |
36 | ron = require 'ron'
37 | client = ron
38 | host: '127.0.0.1'
39 | port: 6379
40 | ```
41 |
42 |
43 |
44 | `get(schema)` Records definition and access
45 | -------------------------------------------
46 | Return a records instance. If the `schema` argument is an object, a new
47 | instance will be created overwriting any previously defined instance
48 | with the same name.
49 |
50 | `schema` An object defining a new schema or a string referencing a schema name.
51 |
52 | Define a record from a object:
53 | ```coffeescript
54 |
55 | client.get
56 | name: 'users'
57 | properties:
58 | user_id: identifier: true
59 | username: unique: true
60 | email: index: true
61 |
62 | ```
63 | Define a record from function calls:
64 | ```coffeescript
65 |
66 | Users = client.get 'users'
67 | Users.identifier 'user_id'
68 | Users.unique 'username'
69 | Users.index 'email'
70 |
71 | ```
72 | Alternatively, the function could be called with a string
73 | followed by multiple schema definition that will be merged.
74 | Here is a valid example:
75 | ```coffeescript
76 |
77 | client.get 'username', temporal: true, properties: username: unique: true
78 | ```
79 |
80 |
81 |
82 | `quit(callback)` Quit
83 | ---------------------
84 | Destroy the redis connection.
85 |
86 | `callback` Received parameters are:
87 |
88 | * `err` Error object if any.
89 | * `status` Status provided by the redis driver
90 |
91 |
--------------------------------------------------------------------------------
/doc/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | language: en
3 | layout: page
4 | title: "Redis ORM for NodeJs"
5 | date: 2012-03-10T15:16:01.006Z
6 | comments: false
7 | sharing: false
8 | footer: false
9 | navigation: ron
10 | github: https://github.com/wdavidw/node-ron
11 | ---
12 |
13 | Redis ORM for NodeJs
14 | ====================
15 |
16 | Installation
17 | ------------
18 |
19 | ```bash
20 | npm install ron
21 | ```
22 |
23 | Usage
24 | -----
25 |
26 | ```javascript
27 | ron = require('ron');
28 | // Client connection
29 | client = ron({
30 | port: 6379
31 | host: '127.0.0.1'
32 | name: 'auth'
33 | });
34 | // Schema definition
35 | Users = client.get('users');
36 | Users.property('id', {identifier: true});
37 | Users.property('username', {unique: true});
38 | Users.property('email', {index: true, type: 'email'});
39 | Users.property('name', {});
40 | // Record manipulation
41 | Users.create(
42 | {username: 'ron', email: 'ron@domain.com'},
43 | function(err, user){
44 | console.log(err, user.id);
45 | }
46 | )
47 | ```
48 |
49 | The library provides
50 | --------------------
51 |
52 | * Documented and tested API
53 | * Records access with indexes and unique values
54 | * Records are pure object, no extended class, no magic
55 |
56 | Client API
57 | ----------
58 |
59 | * [Client::constructor](client.html#ron)
60 | * [Client::get](client.html#get)
61 | * [Client::quit](client.html#quit)
62 |
63 | Schema API
64 | ----------
65 |
66 | * [Records::hash](schema.html#hash)
67 | * [Records::identifier](schema.html#identifier)
68 | * [Records::index](schema.html#index)
69 | * [Records::property](schema.html#property)
70 | * [Records::name](schema.html#name)
71 | * [Records::serialize](schema.html#serialize)
72 | * [Records::temporal](schema.html#temporal)
73 | * [Records::unique](schema.html#unique)
74 | * [Records::unserialize](schema.html#unserialize)
75 | * [Records::validate](schema.html#validate)
76 |
77 | Records API
78 | -----------
79 |
80 | * [Records::all](records.html#all)
81 | * [Records::clear](records.html#clear)
82 | * [Records::count](records.html#count)
83 | * [Records::create](records.html#create)
84 | * [Records::exists](records.html#exists)
85 | * [Records::get](records.html#get)
86 | * [Records::id](records.html#id)
87 | * [Records::list](records.html#list)
88 | * [Records::remove](records.html#remove)
89 | * [Records::update](records.html#update)
90 |
91 | Run tests
92 | ---------
93 |
94 | Start a redis server on the default port
95 | ```bash
96 | redis-server ./conf/redis.conf
97 | ```
98 |
99 | Run the tests with mocha:
100 | ```bash
101 | make test
102 | ```
103 |
104 |
105 |
--------------------------------------------------------------------------------
/doc/records.md:
--------------------------------------------------------------------------------
1 | ---
2 | language: en
3 | layout: page
4 | title: "
5 | Records access and manipulation"
6 | date: 2012-11-15T21:39:55.923Z
7 | comments: false
8 | sharing: false
9 | footer: false
10 | navigation: ron
11 | github: https://github.com/wdavidw/node-ron
12 | ---
13 |
14 |
15 | Implement object based storage with indexing support.
16 |
17 | Identifier
18 | ----------
19 |
20 | Auto generated identifiers are incremented integers. The next identifier is obtained from
21 | a key named as `{s.db}:{s.name}_incr`. All the identifiers are stored as a Redis set in
22 | a key named as `{s.db}:{s.name}_#{identifier}`.
23 |
24 | Data
25 | ----
26 |
27 | Records data is stored as a single hash named as `{s.db}:{s.name}:{idenfitier}`. The hash
28 | keys map to the record properties and the hash value map to the values associated with
29 | each properties.
30 |
31 | Regular indexes
32 | ---------------
33 |
34 | Regular index are stored inside multiple sets, named as
35 | `{s.db}:{s.name}_{property}:{value}`. There is one key for each indexed value and its
36 | associated value is a set containing all the identifiers of the records whose property
37 | match the indexed value.
38 |
39 | Unique indexes
40 | --------------
41 |
42 | Unique indexes are stored inside a single hash key named as
43 | `{s.db}:{s.name}_{property}`. Inside the hash, keys are the unique values
44 | associated to the indexed property and values are the record identifiers.
45 |
46 |
47 | `all(callback)`
48 | ---------------
49 | Return all records. Similar to the find method with far less options
50 | and a faster implementation.
51 |
52 |
53 |
54 | `clear(callback)`
55 | -----------------
56 | Remove all the records and the references poiting to them. This function
57 | takes no other argument than the callback called on error or success.
58 |
59 | `callback` Received parameters are:
60 |
61 | * `err` Error object if any.
62 | * `count` Number of removed records on success
63 |
64 | Usage:
65 | ```coffeescript
66 |
67 | ron.get('users').clear (err, count) ->
68 | return console.error "Failed: #{err.message}" if err
69 | console.log "#{count} records removed"
70 | ```
71 |
72 |
73 |
74 | `count(callback)`
75 | -----------------
76 | Count the number of records present in the database.
77 |
78 | Counting all the records:
79 | ```coffeescript
80 |
81 | Users.count, (err, count) ->
82 | console.log 'count users', count
83 |
84 | ```
85 |
86 | `count(property, values, callback)`
87 | ----------------------------------
88 | Count the number of one or more values for an indexed property.
89 |
90 | Counting multiple values:
91 | ```coffeescript
92 |
93 | Users.get 'users', properties:
94 | user_id: identifier: true
95 | job: index: true
96 | Users.count 'job' [ 'globtrotter', 'icemaker' ], (err, counts) ->
97 | console.log 'count globtrotter', counts[0]
98 | console.log 'count icemaker', counts[1]
99 | ```
100 |
101 |
102 |
103 | `create(records, [options], callback)`
104 | --------------------------------------
105 | Insert one or multiple record. The records must not already exists
106 | in the database or an error will be returned in the callback. Only
107 | the defined properties are inserted.
108 |
109 | The records passed to the function are returned in the callback enriched their new identifier property.
110 |
111 | `records` Record object or array of record objects.
112 |
113 | `options` Options properties include:
114 |
115 | * `identifiers` Return only the created identifiers instead of the records.
116 | * `validate` Validate the records.
117 | * `properties` Array of properties to be returned.
118 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
119 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
120 |
121 | `callback` Called on success or failure. Received parameters are:
122 |
123 | * `err` Error object if any.
124 | * `records` Records with their newly created identifier.
125 |
126 | Records are not validated, it is the responsability of the client program calling `create` to either
127 | call `validate` before calling `create` or to passs the `validate` options.
128 |
129 |
130 |
131 | `exists(records, callback)`
132 | ---------------------------
133 | Check if one or more record exist. The existence of a record is based on its
134 | id or any property defined as unique. The provided callback is called with
135 | an error or the records identifiers. The identifiers respect the same
136 | structure as the provided records argument. If a record does not exists,
137 | its associated return value is null.
138 |
139 | `records` Record object or array of record objects.
140 |
141 | `callback` Called on success or failure. Received parameters are:
142 |
143 | * `err` Error object if any.
144 | * `identifier` Record identifiers or null values.
145 |
146 |
147 |
148 | `get(records, [options], callback)`
149 | -----------------------------------
150 | Retrieve one or multiple records. Records that doesn't exists are returned as null. If
151 | options is an array, it is considered to be the list of properties to retrieve. By default,
152 | unless the `force` option is defined, only the properties not yet defined in the provided
153 | records are fetched from Redis.
154 |
155 | `options` All options are optional. Options properties include:
156 |
157 | * `properties` Array of properties to fetch, all properties unless defined.
158 | * `force` Force the retrieval of properties even if already present in the record objects.
159 | * `accept_null` Skip objects if they are provided as null.
160 | * `object` If `true`, return an object where keys are the identifier and value are the fetched records
161 |
162 | `callback` Called on success or failure. Received parameters are:
163 |
164 | * `err` Error object if the command failed.
165 | * `records` Object or array of object if command succeed. Objects are null if records are not found.
166 |
167 |
168 | `id(records, callback)`
169 | -----------------------
170 | Generate new identifiers. The first arguments `records` may be the number
171 | of ids to generate, a record or an array of records.
172 |
173 |
174 |
175 | `identify(records, [options], callback)`
176 | ----------------------------------------
177 | Extract record identifiers or set the identifier to null if its associated record could not be found.
178 |
179 | The method doesn't hit the database to validate record values and if an id is
180 | provided, it wont check its existence. When a record has no identifier but a unique value, then its
181 | identifier will be fetched from Redis.
182 |
183 | `records` Record object or array of record objects.
184 |
185 | `options` Options properties include:
186 |
187 | * `accept_null` Skip objects if they are provided as null.
188 | * `object` Return an object in the callback even if it recieve an id instead of a record.
189 |
190 | Use reverse index lookup to extract user ids:
191 | ```coffeescript
192 |
193 | Users.get 'users', properties:
194 | user_id: identifier: true
195 | username: unique: true
196 | Users.id [
197 | {username: 'username_1'}
198 | {username: 'username_2'}
199 | ], (err, ids) ->
200 | should.not.exist err
201 | console.log ids
202 |
203 | ```
204 | Use the `object` option to return records instead of ids:
205 | ```coffeescript
206 |
207 | Users.get 'users', properties:
208 | user_id: identifier: true
209 | username: unique: true
210 | Users.id [
211 | 1, {user_id: 2} ,{username: 'username_3'}
212 | ], object: true, (err, users) ->
213 | should.not.exist err
214 | ids = for user in users then user.user_id
215 | console.log ids
216 | ```
217 |
218 |
219 |
220 | `list([options], callback)`
221 | ---------------------------
222 | List records with support for filtering and sorting.
223 |
224 | `options` Options properties include:
225 |
226 | * `direction` One of `asc` or `desc`, default to `asc`.
227 | * `identifiers` Return an array of identifiers instead of the record objects.
228 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
229 | * `properties` Array of properties to be returned.
230 | * `operation` Redis operation in case of multiple `where` properties, default to `union`.
231 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
232 | * `sort` Name of the property by which records should be ordered.
233 | * `where` Hash of property/value used to filter the query.
234 |
235 | `callback` Called on success or failure. Received parameters are:
236 |
237 | * `err` Error object if any.
238 | * `records` Records fetched from Redis.
239 |
240 | Using the `union` operation:
241 | ```coffeescript
242 |
243 | Users.list
244 | where: group: ['admin', 'redis']
245 | operation: 'union'
246 | direction: 'desc'
247 | , (err, users) ->
248 | console.log users
249 |
250 | ```
251 | An alternative syntax is to bypass the `where` option, the exemple above
252 | could be rewritten as:
253 | ```coffeescript
254 |
255 | Users.list
256 | group: ['admin', 'redis']
257 | operation: 'union'
258 | direction: 'desc'
259 | , (err, users) ->
260 | console.log users
261 | ```
262 |
263 |
264 |
265 | `remove(records, callback)`
266 | ---------------------------
267 | Remove one or several records from the database. The function will also
268 | handle all the indexes referencing those records.
269 |
270 | `records` Record object or array of record objects.
271 |
272 | `callback` Called on success or failure. Received parameters are:
273 |
274 | * `err` Error object if any.
275 | * `removed` Number of removed records.
276 |
277 | Removing a single record:
278 | ```coffeescript
279 |
280 | Users.remove id, (err, removed) ->
281 | console.log "#{removed} user removed"
282 | ```
283 |
284 |
285 |
286 | `update(records, [options], callback)`
287 | --------------------------------------
288 | Update one or several records. The records must exists in the database or
289 | an error will be returned in the callback. The existence of a record may
290 | be discovered through its identifier or the presence of a unique property.
291 |
292 | `records` Record object or array of record objects.
293 |
294 | `options` Options properties include:
295 |
296 | * `validate` Validate the records.
297 |
298 | `callback` Called on success or failure. Received parameters are:
299 |
300 | * `err` Error object if any.
301 | * `records` Records with their newly created identifier.
302 |
303 | Records are not validated, it is the responsability of the client program to either
304 | call `validate` before calling `update` or to passs the `validate` options.
305 |
306 | Updating a single record:
307 | ```coffeescript
308 |
309 | Users.update
310 | username: 'my_username'
311 | age: 28
312 | , (err, user) -> console.log user
313 | ```
314 |
315 |
--------------------------------------------------------------------------------
/doc/schema.md:
--------------------------------------------------------------------------------
1 | ---
2 | language: en
3 | layout: page
4 | title: "
5 | Schema definition"
6 | date: 2012-11-15T21:39:55.922Z
7 | comments: false
8 | sharing: false
9 | footer: false
10 | navigation: ron
11 | github: https://github.com/wdavidw/node-ron
12 | ---
13 |
14 |
15 | Schema is a mixin from which `Records` inherits. A schema is defined once
16 | and must no change. We dont support schema migration at the moment. The `Records`
17 | class inherit all the properties and method of the shema.
18 |
19 | `ron` Reference to the Ron instance
20 |
21 | `options` Schema definition. Options include:
22 |
23 | * `name` Name of the schema.
24 | * `properties` Properties definition, an object or an array.
25 |
26 | Record properties may be defined by the following keys:
27 |
28 | * `type` Use to cast the value inside Redis, one of `string`, `int`, `date` or `email`.
29 | * `identifier` Mark this property as the identifier, only one property may be an identifier.
30 | * `index` Create an index on the property.
31 | * `unique` Create a unique index on the property.
32 | * `temporal` Add creation and modification date transparently.
33 |
34 | Define a schema from a configuration object:
35 | ```coffeescript
36 |
37 | ron.get 'users', properties:
38 | user_id: identifier: true
39 | username: unique: true
40 | password: true
41 |
42 | ```
43 | Define a schema with a declarative approach:
44 | ```coffeescript
45 |
46 | Users = ron.get 'users'
47 | Users.indentifier 'user_id'
48 | Users.unique 'username'
49 | Users.property 'password'
50 |
51 | ```
52 | Whichever your style, you can then manipulate your records:
53 | ```coffeescript
54 |
55 | users = ron.get 'users'
56 | users.list (err, users) -> console.log users
57 | ```
58 |
59 |
60 | `hash(key)`
61 | -------------
62 | Utility function used when a redis key is created out of
63 | uncontrolled character (like a string instead of an int).
64 |
65 |
66 |
67 | `identifier(property)`
68 | ------------------------
69 | Define a property as an identifier or return the record identifier if
70 | called without any arguments. An identifier is a property which uniquely
71 | define a record. Inside Redis, identifier values are stored in set.
72 |
73 |
74 |
75 | `index([property])`
76 | -------------------
77 | Define a property as indexable or return all index properties. An
78 | indexed property allow records access by its property value. For example,
79 | when using the `list` function, the search can be filtered such as returned
80 | records match one or multiple values.
81 |
82 | Calling this function without any argument will return an array with all the
83 | indexed properties.
84 |
85 | Example:
86 | ```coffeescript
87 |
88 | User.index 'email'
89 | User.list { filter: { email: 'my@email.com' } }, (err, users) ->
90 | console.log 'This user has the following accounts:'
91 | for user in user
92 | console.log "- #{user.username}"
93 | ```
94 |
95 |
96 |
97 | `property(property, [schema])`
98 | ------------------------------
99 | Define a new property or overwrite the definition of an
100 | existing property. If no schema is provide, return the
101 | property information.
102 |
103 | Calling this function with only the property argument will return the schema
104 | information associated with the property.
105 |
106 | It is possible to define a new property without any schema information by
107 | providing an empty object.
108 |
109 | Example:
110 | ```coffeescript
111 |
112 | User.property 'id', identifier: true
113 | User.property 'username', unique: true
114 | User.property 'email', { index: true, type: 'email' }
115 | User.property 'name', {}
116 | ```
117 |
118 |
119 |
120 | `name()`
121 | --------
122 | Return the schema name of the current instance.
123 |
124 | Using the function :
125 | ```coffeescript
126 | Users = client 'users', properties: username: unique: true
127 | console.log Users.name() is 'users'
128 | ```
129 |
130 |
131 |
132 | `serialize(records)`
133 | --------------------
134 | Cast record values before their insertion into Redis.
135 |
136 | Take a record or an array of records and update values with correct
137 | property types.
138 |
139 |
140 |
141 | `temporal([options])`
142 | ---------------------
143 | Define or retrieve temporal definition. Marking a schema as
144 | temporal will transparently add two new date properties, the
145 | date when the record was created (by default "cdate"), and the date
146 | when the record was last updated (by default "mdate").
147 |
148 |
149 |
150 | `unique([property])`
151 | --------------------
152 | Define a property as unique or retrieve all the unique properties if no
153 | argument is provided. An unique property is similar to a index
154 | property but the index is stored inside a Redis hash. In addition to being
155 | filterable, it could also be used as an identifer to access a record.
156 |
157 | Example:
158 | ```coffeescript
159 |
160 | User.unique 'username'
161 | User.get { username: 'me' }, (err, user) ->
162 | console.log "This is #{user.username}"
163 | ```
164 |
165 |
166 |
167 | `unserialize(records, [options])`
168 | ---------------------------------
169 | Cast record values to their correct type.
170 |
171 | Take a record or an array of records and update values with correct
172 | property types.
173 |
174 | `options` Options include:
175 |
176 | * `identifiers` Return an array of identifiers instead of the record objects.
177 | * `properties` Array of properties to be returned.
178 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
179 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
180 |
181 |
182 |
183 | `validate(records, [options])`
184 | ------------------------------
185 | Validate the properties of one or more records. Return a validation
186 | object or an array of validation objects depending on the provided
187 | records arguments. Keys of a validation object are the name of the invalid
188 | properties and their value is a string indicating the type of error.
189 |
190 | `records` Record object or array of record objects.
191 |
192 | `options` Options include:
193 |
194 | * `throw` Throw errors on first invalid property instead of returning a validation object.
195 | * `skip_required` Doesn't validate missing properties defined as `required`, usefull for partial update.
196 |
197 |
--------------------------------------------------------------------------------
/lib/Client.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.12.3
2 | var Client, Records, Schema, redis;
3 |
4 | redis = require('redis');
5 |
6 | Schema = require('./Schema');
7 |
8 | Records = require('./Records');
9 |
10 | module.exports = Client = (function() {
11 | function Client(options) {
12 | var ref, ref1;
13 | if (options == null) {
14 | options = {};
15 | }
16 | this.options = options;
17 | this.name = options.name || 'ron';
18 | this.schemas = {};
19 | this.records = {};
20 | if (this.options.redis) {
21 | this.redis = this.options.redis;
22 | } else {
23 | this.redis = redis.createClient((ref = options.port) != null ? ref : 6379, (ref1 = options.host) != null ? ref1 : '127.0.0.1');
24 | if (options.password != null) {
25 | this.redis.auth(options.password);
26 | }
27 | if (options.database != null) {
28 | this.redis.select(options.database);
29 | }
30 | }
31 | }
32 |
33 | Client.prototype.get = function(schema) {
34 | var create, i, j, k, ref, ref1, v;
35 | create = true;
36 | if (arguments.length > 1) {
37 | if (typeof arguments[0] === 'string') {
38 | schema = {
39 | name: arguments[0]
40 | };
41 | } else {
42 | schema = arguments[0];
43 | }
44 | for (i = j = 1, ref = arguments.length; 1 <= ref ? j < ref : j > ref; i = 1 <= ref ? ++j : --j) {
45 | ref1 = arguments[i];
46 | for (k in ref1) {
47 | v = ref1[k];
48 | schema[k] = v;
49 | }
50 | }
51 | } else if (typeof schema === 'string') {
52 | schema = {
53 | name: schema
54 | };
55 | if (this.records[schema.name] != null) {
56 | create = false;
57 | }
58 | }
59 | if (create) {
60 | this.records[schema.name] = new Records(this, schema);
61 | }
62 | return this.records[schema.name];
63 | };
64 |
65 | Client.prototype.quit = function(callback) {
66 | return this.redis.quit(function(err, status) {
67 | if (!callback) {
68 | return;
69 | }
70 | if (err) {
71 | return callback(err);
72 | }
73 | if (callback) {
74 | return callback(null, status);
75 | }
76 | });
77 | };
78 |
79 | return Client;
80 |
81 | })();
82 |
--------------------------------------------------------------------------------
/lib/Records.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.12.3
2 | var Records, Schema,
3 | extend = 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; },
4 | hasProp = {}.hasOwnProperty,
5 | slice = [].slice;
6 |
7 | Schema = require('./Schema');
8 |
9 | module.exports = Records = (function(superClass) {
10 | extend(Records, superClass);
11 |
12 | function Records(ron, schema) {
13 | this.redis = ron.redis;
14 | Records.__super__.constructor.call(this, ron, schema);
15 | }
16 |
17 | Records.prototype.all = function(callback) {
18 | var db, identifier, name, redis, ref;
19 | redis = this.redis;
20 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier;
21 | return redis.smembers(db + ":" + name + "_" + identifier, (function(_this) {
22 | return function(err, recordIds) {
23 | var k, len, multi, recordId;
24 | multi = redis.multi();
25 | for (k = 0, len = recordIds.length; k < len; k++) {
26 | recordId = recordIds[k];
27 | multi.hgetall(db + ":" + name + ":" + recordId);
28 | }
29 | return multi.exec(function(err, records) {
30 | if (err) {
31 | return callback(err);
32 | }
33 | _this.unserialize(records);
34 | return callback(null, records);
35 | });
36 | };
37 | })(this));
38 | };
39 |
40 | Records.prototype.clear = function(callback) {
41 | var cmds, count, db, hash, identifier, index, indexProperties, indexSort, k, len, multi, name, property, redis, ref, ref1, unique;
42 | ref = this, redis = ref.redis, hash = ref.hash;
43 | ref1 = this.data, db = ref1.db, name = ref1.name, identifier = ref1.identifier, index = ref1.index, unique = ref1.unique;
44 | cmds = [];
45 | count = 0;
46 | multi = redis.multi();
47 | indexSort = [];
48 | indexProperties = Object.keys(index);
49 | if (indexProperties.length) {
50 | indexSort.push(db + ":" + name + "_" + identifier);
51 | for (k = 0, len = indexProperties.length; k < len; k++) {
52 | property = indexProperties[k];
53 | indexSort.push('get');
54 | indexSort.push(db + ":" + name + ":*->" + property);
55 | cmds.push(['del', db + ":" + name + "_" + property + ":null"]);
56 | }
57 | indexSort.push(function(err, values) {
58 | var i, j, l, ref2, ref3, results1, value;
59 | if (values.length) {
60 | results1 = [];
61 | for (i = l = 0, ref2 = values.length, ref3 = indexProperties.length; ref3 > 0 ? l < ref2 : l > ref2; i = l += ref3) {
62 | results1.push((function() {
63 | var len1, m, results2;
64 | results2 = [];
65 | for (j = m = 0, len1 = indexProperties.length; m < len1; j = ++m) {
66 | property = indexProperties[j];
67 | value = hash(values[i + j]);
68 | results2.push(cmds.push(['del', db + ":" + name + "_" + property + ":" + value]));
69 | }
70 | return results2;
71 | })());
72 | }
73 | return results1;
74 | }
75 | });
76 | multi.sort.apply(multi, indexSort);
77 | }
78 | multi.smembers(db + ":" + name + "_" + identifier, function(err, recordIds) {
79 | var l, len1, recordId, results1;
80 | if (err) {
81 | return callback(err);
82 | }
83 | if (recordIds == null) {
84 | recordIds = [];
85 | }
86 | count = recordIds.length;
87 | for (l = 0, len1 = recordIds.length; l < len1; l++) {
88 | recordId = recordIds[l];
89 | cmds.push(['del', db + ":" + name + ":" + recordId]);
90 | }
91 | cmds.push(['del', db + ":" + name + "_incr"]);
92 | cmds.push(['del', db + ":" + name + "_" + identifier]);
93 | for (property in unique) {
94 | cmds.push(['del', db + ":" + name + "_" + property]);
95 | }
96 | results1 = [];
97 | for (property in index) {
98 | results1.push(cmds.push(['del', db + ":" + name + "_" + property]));
99 | }
100 | return results1;
101 | });
102 | return multi.exec(function(err, results) {
103 | if (err) {
104 | return callback(err);
105 | }
106 | multi = redis.multi(cmds);
107 | return multi.exec(function(err, results) {
108 | if (err) {
109 | return callback(err);
110 | }
111 | return callback(null, count);
112 | });
113 | });
114 | };
115 |
116 | Records.prototype.count = function(callback) {
117 | var db, i, identifier, index, isArray, k, len, multi, name, property, redis, ref, value, values;
118 | redis = this.redis;
119 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier, index = ref.index;
120 | if (arguments.length === 3) {
121 | property = callback;
122 | values = arguments[1];
123 | callback = arguments[2];
124 | if (!index[property]) {
125 | return callback(new Error("Property is not indexed"));
126 | }
127 | isArray = Array.isArray(values);
128 | if (!isArray) {
129 | values = [values];
130 | }
131 | multi = redis.multi();
132 | for (i = k = 0, len = values.length; k < len; i = ++k) {
133 | value = values[i];
134 | value = this.hash(value);
135 | multi.scard(db + ":" + name + "_" + property + ":" + value);
136 | }
137 | return multi.exec(function(err, counts) {
138 | if (err) {
139 | return callback(err);
140 | }
141 | return callback(null, isArray ? counts : counts[0]);
142 | });
143 | } else {
144 | return this.redis.scard(db + ":" + name + "_" + identifier, function(err, count) {
145 | if (err) {
146 | return callback(err);
147 | }
148 | return callback(null, count);
149 | });
150 | }
151 | };
152 |
153 | Records.prototype.create = function(records, options, callback) {
154 | var db, e, hash, identifier, index, isArray, name, properties, redis, ref, ref1, temporal, unique;
155 | if (arguments.length === 2) {
156 | callback = options;
157 | options = {};
158 | }
159 | ref = this, redis = ref.redis, hash = ref.hash;
160 | ref1 = this.data, db = ref1.db, name = ref1.name, temporal = ref1.temporal, properties = ref1.properties, identifier = ref1.identifier, index = ref1.index, unique = ref1.unique;
161 | isArray = Array.isArray(records);
162 | if (!isArray) {
163 | records = [records];
164 | }
165 | if (options.validate) {
166 | try {
167 | this.validate(records, {
168 | "throw": true
169 | });
170 | } catch (error) {
171 | e = error;
172 | return callback(e, (isArray ? records : records[0]));
173 | }
174 | }
175 | return this.exists(records, (function(_this) {
176 | return function(err, recordIds) {
177 | var date, k, len, multi, recordId;
178 | if (err) {
179 | return callback(err);
180 | }
181 | for (k = 0, len = recordIds.length; k < len; k++) {
182 | recordId = recordIds[k];
183 | if (recordId != null) {
184 | return callback(new Error("Record " + recordId + " already exists"));
185 | }
186 | }
187 | multi = redis.multi();
188 | if (temporal != null) {
189 | date = new Date(Date.now());
190 | }
191 | return _this.id(records, function(err, records) {
192 | var i, l, len1, property, r, record, value;
193 | multi = redis.multi();
194 | for (i = l = 0, len1 = records.length; l < len1; i = ++l) {
195 | record = records[i];
196 | if (((temporal != null ? temporal.creation : void 0) != null) && (record[temporal.creation] == null)) {
197 | record[temporal.creation] = date;
198 | }
199 | if (((temporal != null ? temporal.modification : void 0) != null) && (record[temporal.modification] == null)) {
200 | record[temporal.modification] = date;
201 | }
202 | multi.sadd(db + ":" + name + "_" + identifier, record[identifier]);
203 | for (property in unique) {
204 | if (record[property]) {
205 | multi.hset(db + ":" + name + "_" + property, record[property], record[identifier]);
206 | }
207 | }
208 | for (property in index) {
209 | value = record[property];
210 | value = hash(value);
211 | multi.sadd(db + ":" + name + "_" + property + ":" + value, record[identifier]);
212 | }
213 | r = {};
214 | for (property in record) {
215 | value = record[property];
216 | if (!properties[property]) {
217 | continue;
218 | }
219 | if (value != null) {
220 | r[property] = value;
221 | }
222 | }
223 | _this.serialize(r);
224 | multi.hmset(db + ":" + name + ":" + record[identifier], r);
225 | }
226 | return multi.exec(function(err, results) {
227 | var len2, m, result;
228 | if (err) {
229 | return callback(err);
230 | }
231 | for (m = 0, len2 = results.length; m < len2; m++) {
232 | result = results[m];
233 | if (result[0] === !"0") {
234 | return callback(new Error('Corrupted user database '));
235 | }
236 | }
237 | _this.unserialize(records, options);
238 | return callback(null, isArray ? records : records[0]);
239 | });
240 | });
241 | };
242 | })(this));
243 | };
244 |
245 | Records.prototype.exists = function(records, callback) {
246 | var db, identifier, isArray, k, len, multi, name, property, record, recordId, redis, ref, unique;
247 | redis = this.redis;
248 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier, unique = ref.unique;
249 | isArray = Array.isArray(records);
250 | if (!isArray) {
251 | records = [records];
252 | }
253 | multi = redis.multi();
254 | for (k = 0, len = records.length; k < len; k++) {
255 | record = records[k];
256 | if (typeof record === 'object') {
257 | if (record[identifier] != null) {
258 | recordId = record[identifier];
259 | multi.hget(db + ":" + name + ":" + recordId, identifier);
260 | } else {
261 | for (property in unique) {
262 | if (record[property] != null) {
263 | multi.hget(db + ":" + name + "_" + property, record[property]);
264 | }
265 | }
266 | }
267 | } else {
268 | multi.hget(db + ":" + name + ":" + record, identifier);
269 | }
270 | }
271 | return multi.exec((function(_this) {
272 | return function(err, recordIds) {
273 | if (err) {
274 | return callback(err);
275 | }
276 | _this.unserialize(recordIds);
277 | return callback(null, isArray ? recordIds : recordIds[0]);
278 | };
279 | })(this));
280 | };
281 |
282 | Records.prototype.get = function(records, options, callback) {
283 | var db, identifier, isArray, name, redis, ref;
284 | if (arguments.length === 2) {
285 | callback = options;
286 | options = {};
287 | }
288 | if (Array.isArray(options)) {
289 | options = {
290 | properties: options
291 | };
292 | }
293 | redis = this.redis;
294 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier;
295 | isArray = Array.isArray(records);
296 | if (!isArray) {
297 | records = [records];
298 | }
299 | if ((options.accept_null != null) && !records.some(function(record) {
300 | return record !== null;
301 | })) {
302 | return callback(null, isArray ? records : records[0]);
303 | }
304 | return this.identify(records, {
305 | object: true,
306 | accept_null: options.accept_null != null
307 | }, (function(_this) {
308 | return function(err, records) {
309 | var cmds, multi;
310 | if (err) {
311 | return callback(err);
312 | }
313 | cmds = [];
314 | records.forEach(function(record, i) {
315 | var recordId, ref1;
316 | if (record == null) {
317 | return;
318 | }
319 | recordId = record[identifier];
320 | if (recordId === null) {
321 | records[i] = null;
322 | } else if ((ref1 = options.properties) != null ? ref1.length : void 0) {
323 | options.properties.forEach(function(property) {
324 | if (!(options.force || record[property])) {
325 | return cmds.push([
326 | 'hget', db + ":" + name + ":" + recordId, property, function(err, value) {
327 | return record[property] = value;
328 | }
329 | ]);
330 | }
331 | });
332 | } else {
333 | cmds.push([
334 | 'hgetall', db + ":" + name + ":" + recordId, function(err, values) {
335 | var property, results1, value;
336 | results1 = [];
337 | for (property in values) {
338 | value = values[property];
339 | results1.push(record[property] = value);
340 | }
341 | return results1;
342 | }
343 | ]);
344 | }
345 | return cmds.push([
346 | 'exists', db + ":" + name + ":" + recordId, function(err, exists) {
347 | if (!exists) {
348 | return records[i] = null;
349 | }
350 | }
351 | ]);
352 | });
353 | if (cmds.length === 0) {
354 | return callback(null, isArray ? records : records[0]);
355 | }
356 | multi = redis.multi(cmds);
357 | return multi.exec(function(err, values) {
358 | var k, len, record, recordsByIds;
359 | if (err) {
360 | return callback(err);
361 | }
362 | _this.unserialize(records);
363 | if (options.object) {
364 | recordsByIds = {};
365 | for (k = 0, len = records.length; k < len; k++) {
366 | record = records[k];
367 | recordsByIds[record[identifier]] = record;
368 | }
369 | return callback(null, recordsByIds);
370 | } else {
371 | return callback(null, isArray ? records : records[0]);
372 | }
373 | });
374 | };
375 | })(this));
376 | };
377 |
378 | Records.prototype.id = function(records, callback) {
379 | var db, i, identifier, incr, isArray, k, len, name, record, redis, ref, unique;
380 | redis = this.redis;
381 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier, unique = ref.unique;
382 | if (typeof records === 'number') {
383 | incr = records;
384 | isArray = true;
385 | records = (function() {
386 | var k, ref1, results1;
387 | results1 = [];
388 | for (i = k = 0, ref1 = records; 0 <= ref1 ? k < ref1 : k > ref1; i = 0 <= ref1 ? ++k : --k) {
389 | results1.push(null);
390 | }
391 | return results1;
392 | })();
393 | } else {
394 | isArray = Array.isArray(records);
395 | if (!isArray) {
396 | records = [records];
397 | }
398 | incr = 0;
399 | for (k = 0, len = records.length; k < len; k++) {
400 | record = records[k];
401 | if (!record[identifier]) {
402 | incr++;
403 | }
404 | }
405 | }
406 | return redis.incrby(db + ":" + name + "_incr", incr, (function(_this) {
407 | return function(err, recordId) {
408 | var l, len1;
409 | recordId = recordId - incr;
410 | if (err) {
411 | return callback(err);
412 | }
413 | for (i = l = 0, len1 = records.length; l < len1; i = ++l) {
414 | record = records[i];
415 | if (!record) {
416 | records[i] = record = {};
417 | }
418 | if (!record[identifier]) {
419 | recordId++;
420 | }
421 | if (!record[identifier]) {
422 | record[identifier] = recordId;
423 | }
424 | }
425 | return callback(null, isArray ? records : records[0]);
426 | };
427 | })(this));
428 | };
429 |
430 | Records.prototype.identify = function(records, options, callback) {
431 | var cmds, db, err, finalize, i, identifier, isArray, k, len, multi, name, property, record, redis, ref, unique, withUnique;
432 | if (arguments.length === 2) {
433 | callback = options;
434 | options = {};
435 | }
436 | redis = this.redis;
437 | ref = this.data, db = ref.db, name = ref.name, identifier = ref.identifier, unique = ref.unique;
438 | isArray = Array.isArray(records);
439 | if (!isArray) {
440 | records = [records];
441 | }
442 | cmds = [];
443 | err = null;
444 | for (i = k = 0, len = records.length; k < len; i = ++k) {
445 | record = records[i];
446 | if (typeof record === 'object') {
447 | if (record == null) {
448 | if (!options.accept_null) {
449 | return callback(new Error('Null record'));
450 | }
451 | } else if (record[identifier] != null) {
452 |
453 | } else {
454 | withUnique = false;
455 | for (property in unique) {
456 | if (record[property] != null) {
457 | withUnique = true;
458 | cmds.push([
459 | 'hget', db + ":" + name + "_" + property, record[property], (function(record) {
460 | return function(err, recordId) {
461 | return record[identifier] = recordId;
462 | };
463 | })(record)
464 | ]);
465 | }
466 | }
467 | if (!withUnique) {
468 | return callback(new Error('Invalid record, got ' + (JSON.stringify(record))));
469 | }
470 | }
471 | } else if (typeof record === 'number' || typeof record === 'string') {
472 | records[i] = {};
473 | records[i][identifier] = record;
474 | } else {
475 | return callback(new Error('Invalid id, got ' + (JSON.stringify(record))));
476 | }
477 | }
478 | finalize = function() {
479 | if (!options.object) {
480 | records = (function() {
481 | var l, len1, results1;
482 | results1 = [];
483 | for (l = 0, len1 = records.length; l < len1; l++) {
484 | record = records[l];
485 | if (record != null) {
486 | results1.push(record[identifier]);
487 | } else {
488 | results1.push(record);
489 | }
490 | }
491 | return results1;
492 | })();
493 | }
494 | return callback(null, isArray ? records : records[0]);
495 | };
496 | if (cmds.length === 0) {
497 | return finalize();
498 | }
499 | multi = redis.multi(cmds);
500 | return multi.exec((function(_this) {
501 | return function(err, results) {
502 | if (err) {
503 | return callback(err);
504 | }
505 | _this.unserialize(records);
506 | return finalize();
507 | };
508 | })(this));
509 | };
510 |
511 | Records.prototype.list = function(options, callback) {
512 | var args, db, filter, hash, identifier, index, k, keys, l, len, len1, len2, m, multi, name, operation, property, redis, ref, ref1, ref2, ref3, ref4, ref5, tempkey, v, value, where;
513 | if (typeof options === 'function') {
514 | callback = options;
515 | options = {};
516 | }
517 | ref = this, redis = ref.redis, hash = ref.hash;
518 | ref1 = this.data, db = ref1.db, name = ref1.name, identifier = ref1.identifier, index = ref1.index;
519 | options.properties = options.properties || Object.keys(this.data.properties);
520 | if (options.identifiers) {
521 | options.properties = [identifier];
522 | }
523 | args = [];
524 | multi = this.redis.multi();
525 | if (options.where == null) {
526 | options.where = {};
527 | }
528 | where = [];
529 | for (property in options) {
530 | value = options[property];
531 | if (index[property]) {
532 | if (Array.isArray(value)) {
533 | for (k = 0, len = value.length; k < len; k++) {
534 | v = value[k];
535 | where.push([property, v]);
536 | }
537 | } else {
538 | where.push([property, value]);
539 | }
540 | }
541 | }
542 | options.where = Object.keys(options.where).length ? options.where : false;
543 | if (where.length === 1) {
544 | ref2 = where[0], property = ref2[0], value = ref2[1];
545 | value = hash(value);
546 | args.push(db + ":" + name + "_" + property + ":" + value);
547 | } else if (where.length > 1) {
548 | tempkey = "temp:" + ((new Date).getTime()) + (Math.random());
549 | keys = [];
550 | keys.push(tempkey);
551 | args.push(tempkey);
552 | for (l = 0, len1 = where.length; l < len1; l++) {
553 | filter = where[l];
554 | property = filter[0], value = filter[1];
555 | value = hash(value);
556 | keys.push(db + ":" + name + "_" + property + ":" + value);
557 | }
558 | operation = (ref3 = options.operation) != null ? ref3 : 'union';
559 | multi["s" + operation + "store"].apply(multi, keys);
560 | } else {
561 | args.push(db + ":" + name + "_" + identifier);
562 | }
563 | if (options.sort != null) {
564 | args.push('by');
565 | args.push((db + ":" + name + ":*->") + options.sort);
566 | }
567 | ref4 = options.properties;
568 | for (m = 0, len2 = ref4.length; m < len2; m++) {
569 | property = ref4[m];
570 | args.push('get');
571 | args.push((db + ":" + name + ":*->") + property);
572 | }
573 | args.push('alpha');
574 | args.push((ref5 = options.direction) != null ? ref5 : 'asc');
575 | args.push((function(_this) {
576 | return function(err, values) {
577 | var i, j, record, result;
578 | if (err) {
579 | return callback(err);
580 | }
581 | if (!values.length) {
582 | return callback(null, []);
583 | }
584 | result = (function() {
585 | var len3, n, o, ref6, ref7, ref8, results1;
586 | results1 = [];
587 | for (i = n = 0, ref6 = values.length, ref7 = options.properties.length; ref7 > 0 ? n < ref6 : n > ref6; i = n += ref7) {
588 | record = {};
589 | ref8 = options.properties;
590 | for (j = o = 0, len3 = ref8.length; o < len3; j = ++o) {
591 | property = ref8[j];
592 | record[property] = values[i + j];
593 | }
594 | results1.push(this.unserialize(record, options));
595 | }
596 | return results1;
597 | }).call(_this);
598 | return callback(null, result);
599 | };
600 | })(this));
601 | multi.sort.apply(multi, args);
602 | if (tempkey) {
603 | multi.del(tempkey);
604 | }
605 | return multi.exec();
606 | };
607 |
608 | Records.prototype.remove = function(records, callback) {
609 | var db, hash, identifier, index, isArray, name, redis, ref, ref1, removed, unique;
610 | ref = this, redis = ref.redis, hash = ref.hash;
611 | ref1 = this.data, db = ref1.db, name = ref1.name, identifier = ref1.identifier, index = ref1.index, unique = ref1.unique;
612 | isArray = Array.isArray(records);
613 | if (!isArray) {
614 | records = [records];
615 | }
616 | removed = 0;
617 | return this.get(records, [].concat(Object.keys(unique), Object.keys(index)), function(err, records) {
618 | var fn, k, len, multi, record;
619 | if (err) {
620 | return callback(err);
621 | }
622 | multi = redis.multi();
623 | fn = function(record) {
624 | var property, recordId, results1, value;
625 | recordId = record[identifier];
626 | multi.del(db + ":" + name + ":" + recordId, function(err) {
627 | return removed++;
628 | });
629 | multi.srem(db + ":" + name + "_" + identifier, recordId);
630 | for (property in unique) {
631 | multi.hdel(db + ":" + name + "_" + property, record[property]);
632 | }
633 | results1 = [];
634 | for (property in index) {
635 | value = hash(record[property]);
636 | results1.push(multi.srem(db + ":" + name + "_" + property + ":" + value, recordId, function(err, count) {
637 | if (count !== 1) {
638 | return console.warn('Missing indexed property');
639 | }
640 | }));
641 | }
642 | return results1;
643 | };
644 | for (k = 0, len = records.length; k < len; k++) {
645 | record = records[k];
646 | if (record === null) {
647 | continue;
648 | }
649 | fn(record);
650 | }
651 | return multi.exec(function(err, results) {
652 | if (err) {
653 | return callback(err);
654 | }
655 | return callback(null, removed);
656 | });
657 | });
658 | };
659 |
660 | Records.prototype.update = function(records, options, callback) {
661 | var db, e, hash, identifier, index, isArray, name, properties, redis, ref, ref1, temporal, unique;
662 | if (arguments.length === 2) {
663 | callback = options;
664 | options = {};
665 | }
666 | ref = this, redis = ref.redis, hash = ref.hash;
667 | ref1 = this.data, db = ref1.db, name = ref1.name, temporal = ref1.temporal, properties = ref1.properties, identifier = ref1.identifier, unique = ref1.unique, index = ref1.index;
668 | isArray = Array.isArray(records);
669 | if (!isArray) {
670 | records = [records];
671 | }
672 | if (options.validate) {
673 | try {
674 | this.validate(records, {
675 | "throw": true,
676 | skip_required: true
677 | });
678 | } catch (error) {
679 | e = error;
680 | return callback(e, (isArray ? records : records[0]));
681 | }
682 | }
683 | return this.identify(records, {
684 | object: true
685 | }, (function(_this) {
686 | return function(err, records) {
687 | var cmdsCheck, cmdsUpdate, fn, k, l, len, len1, multi, property, r, record, recordId, value;
688 | if (err) {
689 | return callback(err);
690 | }
691 | for (k = 0, len = records.length; k < len; k++) {
692 | record = records[k];
693 | if (!record) {
694 | return callback(new Error('Invalid record'));
695 | }
696 | }
697 | cmdsCheck = [];
698 | cmdsUpdate = [];
699 | multi = redis.multi();
700 | fn = function(record) {
701 | var len2, m, potentiallyChangedProperties, property, recordId, ref2;
702 | recordId = record[identifier];
703 | potentiallyChangedProperties = [];
704 | ref2 = [].concat(Object.keys(unique), Object.keys(index));
705 | for (m = 0, len2 = ref2.length; m < len2; m++) {
706 | property = ref2[m];
707 | if (typeof record[property] !== 'undefined') {
708 | potentiallyChangedProperties.push(property);
709 | }
710 | }
711 | if (potentiallyChangedProperties.length) {
712 | return multi.hmget.apply(multi, [db + ":" + name + ":" + recordId].concat(slice.call(potentiallyChangedProperties), [function(err, values) {
713 | var len3, n, propertyI, results1, valueNew, valueOld;
714 | results1 = [];
715 | for (propertyI = n = 0, len3 = potentiallyChangedProperties.length; n < len3; propertyI = ++n) {
716 | property = potentiallyChangedProperties[propertyI];
717 | if (values[propertyI] !== record[property]) {
718 | if (properties[property].unique) {
719 | cmdsCheck.push(['hexists', db + ":" + name + "_" + property, record[property]]);
720 | cmdsUpdate.push(['hdel', db + ":" + name + "_" + property, values[propertyI]]);
721 | results1.push(cmdsUpdate.push([
722 | 'hsetnx', db + ":" + name + "_" + property, record[property], recordId, function(err, success) {
723 | if (!success) {
724 | return console.warn('Trying to write on existing unique property');
725 | }
726 | }
727 | ]));
728 | } else if (properties[property].index) {
729 | valueOld = hash(values[propertyI]);
730 | valueNew = hash(record[property]);
731 | cmdsUpdate.push(['srem', db + ":" + name + "_" + property + ":" + valueOld, recordId]);
732 | results1.push(cmdsUpdate.push(['sadd', db + ":" + name + "_" + property + ":" + valueNew, recordId]));
733 | } else {
734 | results1.push(void 0);
735 | }
736 | } else {
737 | results1.push(void 0);
738 | }
739 | }
740 | return results1;
741 | }]));
742 | }
743 | };
744 | for (l = 0, len1 = records.length; l < len1; l++) {
745 | record = records[l];
746 | recordId = record[identifier];
747 | if (!recordId) {
748 | return callback(new Error('Unsaved record'));
749 | }
750 | if (((temporal != null ? temporal.modification : void 0) != null) && (record[temporal.modification] == null)) {
751 | record[temporal.modification] = new Date(Date.now());
752 | }
753 | r = {};
754 | for (property in record) {
755 | value = record[property];
756 | if (value != null) {
757 | r[property] = value;
758 | } else {
759 | cmdsUpdate.push(['hdel', db + ":" + name + ":" + recordId, property]);
760 | }
761 | }
762 | _this.serialize(r);
763 | cmdsUpdate.push(['hmset', db + ":" + name + ":" + recordId, r]);
764 | fn(record);
765 | }
766 | return multi.exec(function(err, values) {
767 | multi = redis.multi(cmdsCheck);
768 | return multi.exec(function(err, exists) {
769 | var exist, len2, m;
770 | if (err) {
771 | return callback(err);
772 | }
773 | for (m = 0, len2 = exists.length; m < len2; m++) {
774 | exist = exists[m];
775 | if (exist !== 0) {
776 | return callback(new Error('Unique value already exists'));
777 | }
778 | }
779 | multi = redis.multi(cmdsUpdate);
780 | return multi.exec(function(err, results) {
781 | if (err) {
782 | return callback(err);
783 | }
784 | return callback(null, isArray ? records : records[0]);
785 | });
786 | });
787 | });
788 | };
789 | })(this));
790 | };
791 |
792 | return Records;
793 |
794 | })(Schema);
795 |
--------------------------------------------------------------------------------
/lib/Schema.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.12.3
2 | var Schema, crypto, isEmail;
3 |
4 | crypto = require('crypto');
5 |
6 | isEmail = function(email) {
7 | return /^[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+(\.[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+)*@[a-z0-9\-]+(\.[a-z0-9\-]+)*\.([a-z]{2,})$/.test(email);
8 | };
9 |
10 | module.exports = Schema = (function() {
11 | function Schema(ron, options) {
12 | var name, ref, value;
13 | this.ron = ron;
14 | if (typeof options === 'string') {
15 | options = {
16 | name: options
17 | };
18 | }
19 | this.data = {
20 | db: ron.name,
21 | name: options.name,
22 | temporal: {},
23 | properties: {},
24 | identifier: null,
25 | index: {},
26 | unique: {}
27 | };
28 | if (options.temporal) {
29 | this.temporal(options.temporal);
30 | }
31 | if (options.properties) {
32 | ref = options.properties;
33 | for (name in ref) {
34 | value = ref[name];
35 | this.property(name, value);
36 | }
37 | }
38 | }
39 |
40 | Schema.prototype.hash = function(key) {
41 | if (typeof key === 'number') {
42 | key = "" + key;
43 | }
44 | if (key != null) {
45 | return crypto.createHash('sha1').update(key).digest('hex');
46 | } else {
47 | return 'null';
48 | }
49 | };
50 |
51 | Schema.prototype.identifier = function(property) {
52 | if (property != null) {
53 | if (this.data.properties[property] == null) {
54 | this.data.properties[property] = {};
55 | }
56 | this.data.properties[property].type = 'int';
57 | this.data.properties[property].identifier = true;
58 | this.data.identifier = property;
59 | return this;
60 | } else {
61 | return this.data.identifier;
62 | }
63 | };
64 |
65 | Schema.prototype.index = function(property) {
66 | if (property != null) {
67 | if (this.data.properties[property] == null) {
68 | this.data.properties[property] = {};
69 | }
70 | this.data.properties[property].index = true;
71 | this.data.index[property] = true;
72 | return this;
73 | } else {
74 | return Object.keys(this.data.index);
75 | }
76 | };
77 |
78 | Schema.prototype.property = function(property, schema) {
79 | if (schema != null) {
80 | if (schema == null) {
81 | schema = {};
82 | }
83 | schema.name = property;
84 | this.data.properties[property] = schema;
85 | if (schema.identifier) {
86 | this.identifier(property);
87 | }
88 | if (schema.index) {
89 | this.index(property);
90 | }
91 | if (schema.unique) {
92 | this.unique(property);
93 | }
94 | return this;
95 | } else {
96 | return this.data.properties[property];
97 | }
98 | };
99 |
100 | Schema.prototype.name = function() {
101 | return this.data.name;
102 | };
103 |
104 | Schema.prototype.serialize = function(records) {
105 | var i, isArray, j, len, properties, property, record, ref, value;
106 | properties = this.data.properties;
107 | isArray = Array.isArray(records);
108 | if (!isArray) {
109 | records = [records];
110 | }
111 | for (i = j = 0, len = records.length; j < len; i = ++j) {
112 | record = records[i];
113 | if (record == null) {
114 | continue;
115 | }
116 | if (typeof record === 'object') {
117 | for (property in record) {
118 | value = record[property];
119 | if (((ref = properties[property]) != null ? ref.type : void 0) === 'date' && (value != null)) {
120 | if (typeof value === 'number') {
121 |
122 | } else if (typeof value === 'string') {
123 | if (/^\d+$/.test(value)) {
124 | record[property] = parseInt(value, 10);
125 | } else {
126 | record[property] = Date.parse(value);
127 | }
128 | } else if (typeof value === 'object' && value instanceof Date) {
129 | record[property] = value.getTime();
130 | }
131 | }
132 | }
133 | }
134 | }
135 | if (isArray) {
136 | return records;
137 | } else {
138 | return records[0];
139 | }
140 | };
141 |
142 | Schema.prototype.temporal = function(temporal) {
143 | if (temporal != null) {
144 | if (temporal === true) {
145 | temporal = {
146 | creation: 'cdate',
147 | modification: 'mdate'
148 | };
149 | }
150 | this.data.temporal = temporal;
151 | this.property(temporal.creation, {
152 | type: 'date'
153 | });
154 | return this.property(temporal.modification, {
155 | type: 'date'
156 | });
157 | } else {
158 | return [this.data.temporal.creation, this.data.temporal.modification];
159 | }
160 | };
161 |
162 | Schema.prototype.unique = function(property) {
163 | if (property != null) {
164 | if (this.data.properties[property] == null) {
165 | this.data.properties[property] = {};
166 | }
167 | this.data.properties[property].unique = true;
168 | this.data.unique[property] = true;
169 | return this;
170 | } else {
171 | return Object.keys(this.data.unique);
172 | }
173 | };
174 |
175 | Schema.prototype.unserialize = function(records, options) {
176 | var i, identifier, isArray, j, len, properties, property, record, ref, ref1, ref2, value;
177 | if (options == null) {
178 | options = {};
179 | }
180 | ref = this.data, identifier = ref.identifier, properties = ref.properties;
181 | isArray = Array.isArray(records);
182 | if (!isArray) {
183 | records = [records];
184 | }
185 | if (options.identifiers) {
186 | options.properties = [identifier];
187 | }
188 | for (i = j = 0, len = records.length; j < len; i = ++j) {
189 | record = records[i];
190 | if (record == null) {
191 | continue;
192 | }
193 | if (typeof record === 'object') {
194 | for (property in record) {
195 | value = record[property];
196 | if (options.properties && options.properties.indexOf(property) === -1) {
197 | delete record[property];
198 | continue;
199 | }
200 | if (((ref1 = properties[property]) != null ? ref1.type : void 0) === 'int' && (value != null)) {
201 | record[property] = parseInt(value, 10);
202 | } else if (((ref2 = properties[property]) != null ? ref2.type : void 0) === 'date' && (value != null)) {
203 | if (/^\d+$/.test(value)) {
204 | value = parseInt(value, 10);
205 | } else {
206 | value = Date.parse(value);
207 | }
208 | if (options.milliseconds) {
209 | record[property] = value;
210 | } else if (options.seconds) {
211 | record[property] = Math.round(value / 1000);
212 | } else {
213 | record[property] = new Date(value);
214 | }
215 | }
216 | }
217 | if (options.identifiers) {
218 | records[i] = record[identifier];
219 | }
220 | } else if (typeof record === 'number' || typeof record === 'string') {
221 | records[i] = parseInt(record);
222 | }
223 | }
224 | if (isArray) {
225 | return records;
226 | } else {
227 | return records[0];
228 | }
229 | };
230 |
231 | Schema.prototype.validate = function(records, options) {
232 | var db, isArray, name, properties, property, record, ref, validation, validations, x;
233 | if (options == null) {
234 | options = {};
235 | }
236 | ref = this.data, db = ref.db, name = ref.name, properties = ref.properties;
237 | isArray = Array.isArray(records);
238 | if (!isArray) {
239 | records = [records];
240 | }
241 | validations = (function() {
242 | var j, len, results;
243 | results = [];
244 | for (j = 0, len = records.length; j < len; j++) {
245 | record = records[j];
246 | validation = {};
247 | for (x in properties) {
248 | property = properties[x];
249 | if (!options.skip_required && property.required && (record[property.name] == null)) {
250 | if (options["throw"]) {
251 | throw new Error("Required property " + property.name);
252 | } else {
253 | validation[property.name] = 'required';
254 | }
255 | } else if (property.type === 'email' && !isEmail(record[property.name])) {
256 | if (options["throw"]) {
257 | throw new Error("Invalid email " + record[property.name]);
258 | } else {
259 | validation[property.name] = 'invalid_email';
260 | }
261 | }
262 | }
263 | results.push(validation);
264 | }
265 | return results;
266 | })();
267 | if (isArray) {
268 | return validations;
269 | } else {
270 | return validations[0];
271 | }
272 | };
273 |
274 | return Schema;
275 |
276 | })();
277 |
--------------------------------------------------------------------------------
/lib/doc.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.12.3
2 | var convert_anchor, convert_code, date, each, fs, mecano;
3 |
4 | fs = require('fs');
5 |
6 | mecano = require('mecano');
7 |
8 | each = require('each');
9 |
10 | date = function() {
11 | var d;
12 | return d = (new Date).toISOString();
13 | };
14 |
15 | convert_anchor = function(text) {
16 | var re_anchor;
17 | re_anchor = /`([\w]+)\(/g;
18 | return text.replace(re_anchor, function(str, code) {
19 | return "\n`" + code + "(";
20 | });
21 | };
22 |
23 | convert_code = function(text) {
24 | var re_code;
25 | re_code = /\n(\s{2}\s*?\w[\s\S]*?)\n(?!\s)/g;
26 | return text.replace(re_code, function(str, code) {
27 | code = code.split('\n').map(function(line) {
28 | return line.substr(4);
29 | }).join('\n');
30 | return "\n```coffeescript\n" + code + "\n```\n";
31 | });
32 | };
33 |
34 | each(['Client', 'Schema', 'Records']).parallel(true).on('item', function(file, next) {
35 | var destination, source;
36 | source = __dirname + "/" + file + ".coffee";
37 | destination = __dirname + "/../doc/" + (file.toLowerCase()) + ".md";
38 | return fs.readFile(source, 'ascii', function(err, content) {
39 | var docs, match, re, re_title;
40 | if (err) {
41 | return console.error(err);
42 | }
43 | re = /###\n([\s\S]*?)\n( *)###/g;
44 | re_title = /([\s\S]+)\n={2}=+([\s\S]*)/g;
45 | match = re.exec(content);
46 | match = re_title.exec(match[1]);
47 | 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]));
48 | while (match = re.exec(content)) {
49 | match[1] = match[1].split('\n').map(function(line) {
50 | return line.substr(2);
51 | }).join('\n');
52 | docs += convert_code(convert_anchor(match[1]));
53 | docs += '\n';
54 | }
55 | return fs.writeFile(destination, docs, next);
56 | });
57 | }).on('both', function(err) {
58 | var destination;
59 | if (err) {
60 | return console.error(err);
61 | }
62 | console.log('Documentation generated');
63 | destination = process.argv[2];
64 | if (!destination) {
65 | return;
66 | }
67 | return each(['index', 'client', 'schema', 'records']).on('item', function(next, file) {
68 | return mecano.copy({
69 | source: __dirname + "/../doc/" + file + ".md",
70 | destination: destination,
71 | force: true
72 | }, next);
73 | }).on('both', function(err) {
74 | if (err) {
75 | return console.error(err);
76 | }
77 | return console.log('Documentation published');
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.12.3
2 | var Client;
3 |
4 | Client = require('./Client');
5 |
6 | module.exports = function(options) {
7 | return new Client(options);
8 | };
9 |
10 | module.exports.Client = Client;
11 |
12 | module.exports.Records = require('./Records');
13 |
14 | module.exports.Schema = require('./Schema');
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ron",
3 | "version": "0.0.8",
4 | "description": "Redis ORM for NodeJs.",
5 | "keywords": ["redis", "orm", "database", "nosql"],
6 | "homepage": "http://www.adaltas.com/projects/node-ron",
7 | "license": "BSD-3-Clause",
8 | "author": "David Worms ",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/adaltas/node-ron"
12 | },
13 | "bugs": {
14 | "email": "david@adaltas.com",
15 | "url": "http://github.com/adaltas/node-ron/issues"
16 | },
17 | "dependencies": {
18 | "redis": "^2.6.5"
19 | },
20 | "devDependencies": {
21 | "coffee-script": "^1.11.1",
22 | "each": "^0.6.1",
23 | "mocha": "^2.4.5",
24 | "should": "^11.1.0"
25 | },
26 | "contributors": [
27 | { "name": "David Worms", "email": "david@adaltas.com" }
28 | ],
29 | "main": "lib/index",
30 | "engines": { "node": ">= 0.4.0" },
31 | "scripts": {
32 | "coffee": "coffee -b -o lib src",
33 | "redis_start": "docker run --name ron -p 6379:6379 -d redis",
34 | "redis_stop": "docker rm -f ron",
35 | "pretest": "cp -n conf/test.coffee.sample conf/test.coffee; coffee -b -o lib src",
36 | "test": "mocha"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/samples/create.js:
--------------------------------------------------------------------------------
1 |
2 | ron = require('..');
3 | // Client connection
4 | client = ron({
5 | port: 6379,
6 | host: '127.0.0.1',
7 | name: 'auth'
8 | });
9 | // Schema definition
10 | Users = client.get('users');
11 | Users.property('id', {identifier: true});
12 | Users.property('username', {unique: true});
13 | Users.property('email', {index: true, type: 'email'});
14 | Users.property('name', {});
15 | // Record manipulation
16 | Users.create(
17 | {username: 'ron', email: 'ron@domain.com'},
18 | function(err, user){
19 | console.log(err, user.id);
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/src/Client.coffee.md:
--------------------------------------------------------------------------------
1 |
2 | # Client connection
3 |
4 | The client wraps a redis connection and provides access to records definition
5 | and manipulation.
6 |
7 | Internally, Ron use the [Redis client for Node.js](https://github.com/mranney/node_redis).
8 |
9 | ## Dependencies
10 |
11 | redis = require 'redis'
12 | Schema = require './Schema'
13 | Records = require './Records'
14 |
15 | ## Module definition
16 |
17 | module.exports = class Client
18 |
19 |
20 | ## `ron([options])` Client creation
21 |
22 | `options` Options properties include:
23 |
24 | * `name` A namespace for the application, all keys with be prefixed with "#{name}:". Default to "ron"
25 | * `redis` Provide an existing instance in case you don't want a new one to be created.
26 | * `host` Redis hostname.
27 | * `port` Redis port.
28 | * `password` Redis password.
29 | * `database` Redis database (an integer).
30 |
31 | Basic example:
32 |
33 | ```coffee
34 | ron = require 'ron'
35 | client = ron
36 | host: '127.0.0.1'
37 | port: 6379
38 | ```
39 |
40 | constructor: (options = {}) ->
41 | @options = options
42 | @name = options.name or 'ron'
43 | @schemas = {}
44 | @records = {}
45 | if @options.redis
46 | @redis = @options.redis
47 | else
48 | @redis = redis.createClient options.port ? 6379, options.host ? '127.0.0.1'
49 | @redis.auth options.password if options.password?
50 | @redis.select options.database if options.database?
51 |
52 | ## `get(schema)` Records definition and access
53 |
54 | Return a records instance. If the `schema` argument is an object, a new
55 | instance will be created overwriting any previously defined instance
56 | with the same name.
57 |
58 | `schema` An object defining a new schema or a string referencing a schema name.
59 |
60 | Define a record from a object:
61 |
62 | ```coffee
63 | client.get
64 | name: 'users'
65 | properties:
66 | user_id: identifier: true
67 | username: unique: true
68 | email: index: true
69 | ```
70 |
71 | Define a record from function calls:
72 |
73 | ```coffee
74 | Users = client.get 'users'
75 | Users.identifier 'user_id'
76 | Users.unique 'username'
77 | Users.index 'email'
78 | ```
79 |
80 | Alternatively, the function could be called with a string
81 | followed by multiple schema definition that will be merged.
82 | Here is a valid example:
83 |
84 | ```coffee
85 | client.get 'username', temporal: true, properties: username: unique: true
86 | ```
87 |
88 | get: (schema) ->
89 | create = true
90 | if arguments.length > 1
91 | if typeof arguments[0] is 'string'
92 | then schema = name: arguments[0]
93 | else schema = arguments[0]
94 | for i in [1 ... arguments.length]
95 | for k, v of arguments[i]
96 | schema[k] = v
97 | else if typeof schema is 'string'
98 | schema = {name: schema}
99 | create = false if @records[schema.name]?
100 | @records[schema.name] = new Records @, schema if create
101 | @records[schema.name]
102 |
103 | ## `quit(callback)` Quit
104 |
105 | Destroy the redis connection.
106 |
107 | `callback` Received parameters are:
108 |
109 | * `err` Error object if any.
110 | * `status` Status provided by the redis driver
111 |
112 | quit: (callback) ->
113 | @redis.quit (err, status) ->
114 | return unless callback
115 | return callback err if err
116 | callback null, status if callback
117 |
--------------------------------------------------------------------------------
/src/Records.coffee.md:
--------------------------------------------------------------------------------
1 |
2 | # Records access and manipulation
3 |
4 | Implement object based storage with indexing support.
5 |
6 | ## Dependencies
7 |
8 | Schema = require './Schema'
9 |
10 | ## Identifier
11 |
12 | Auto generated identifiers are incremented integers. The next identifier is obtained from
13 | a key named as `{s.db}:{s.name}_incr`. All the identifiers are stored as a Redis set in
14 | a key named as `{s.db}:{s.name}_#{identifier}`.
15 |
16 | ## Data
17 |
18 | Records data is stored as a single hash named as `{s.db}:{s.name}:{idenfitier}`. The hash
19 | keys map to the record properties and the hash value map to the values associated with
20 | each properties.
21 |
22 | ## Regular indexes
23 |
24 | Regular index are stored inside multiple sets, named as
25 | `{s.db}:{s.name}_{property}:{value}`. There is one key for each indexed value and its
26 | associated value is a set containing all the identifiers of the records whose property
27 | match the indexed value.
28 |
29 | ## Unique indexes
30 |
31 | Unique indexes are stored inside a single hash key named as
32 | `{s.db}:{s.name}_{property}`. Inside the hash, keys are the unique values
33 | associated to the indexed property and values are the record identifiers.
34 |
35 | ## Module definition
36 |
37 | module.exports = class Records extends Schema
38 |
39 | constructor: (ron, schema) ->
40 | @redis = ron.redis
41 | super ron, schema
42 |
43 | ## `all(callback)`
44 |
45 | Return all records. Similar to the find method with far less options
46 | and a faster implementation.
47 |
48 | all: (callback) ->
49 | {redis} = @
50 | {db, name, identifier} = @data
51 | redis.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) =>
52 | multi = redis.multi()
53 | for recordId in recordIds
54 | multi.hgetall "#{db}:#{name}:#{recordId}"
55 | multi.exec (err, records) =>
56 | return callback err if err
57 | @unserialize records
58 | callback null, records
59 |
60 | ## `clear(callback)`
61 |
62 | Remove all the records and the references poiting to them. This function
63 | takes no other argument than the callback called on error or success.
64 |
65 | `callback` Received parameters are:
66 |
67 | * `err` Error object if any.
68 | * `count` Number of removed records on success
69 |
70 | Usage:
71 |
72 | ```coffee
73 | ron.get('users').clear (err, count) ->
74 | return console.error "Failed: #{err.message}" if err
75 | console.log "#{count} records removed"
76 | ```
77 |
78 | clear: (callback) ->
79 | {redis, hash} = @
80 | {db, name, identifier, index, unique} = @data
81 | cmds = []
82 | count = 0
83 | multi = redis.multi()
84 | # Grab index values for later removal
85 | indexSort = []
86 | indexProperties = Object.keys(index)
87 | if indexProperties.length
88 | indexSort.push "#{db}:#{name}_#{identifier}"
89 | for property in indexProperties
90 | indexSort.push 'get'
91 | indexSort.push "#{db}:#{name}:*->#{property}"
92 | # Delete null index
93 | cmds.push ['del', "#{db}:#{name}_#{property}:null"]
94 | indexSort.push (err, values) ->
95 | if values.length
96 | for i in [0 ... values.length] by indexProperties.length
97 | for property, j in indexProperties
98 | value = hash values[i + j]
99 | cmds.push ['del', "#{db}:#{name}_#{property}:#{value}"]
100 | multi.sort indexSort...
101 | # Grab record identifiers
102 | multi.smembers "#{db}:#{name}_#{identifier}", (err, recordIds) ->
103 | return callback err if err
104 | # Return count in final callback
105 | # console.log 'recordIds', err, recordIds
106 | recordIds ?= []
107 | count = recordIds.length
108 | # delete objects
109 | for recordId in recordIds
110 | cmds.push ['del', "#{db}:#{name}:#{recordId}"]
111 | # Incremental counter
112 | cmds.push ['del', "#{db}:#{name}_incr"]
113 | # Identifier index
114 | cmds.push ['del', "#{db}:#{name}_#{identifier}"]
115 | # Unique indexes
116 | for property of unique
117 | cmds.push ['del', "#{db}:#{name}_#{property}"]
118 | # Index of values
119 | for property of index
120 | cmds.push ['del', "#{db}:#{name}_#{property}"]
121 | multi.exec (err, results) ->
122 | return callback err if err
123 | multi = redis.multi cmds
124 | multi.exec (err, results) ->
125 | return callback err if err
126 | callback null, count
127 |
128 | ## `count(callback)`
129 |
130 | Count the number of records present in the database.
131 |
132 | Counting all the records:
133 |
134 | ```coffee
135 | Users.count, (err, count) ->
136 | console.log 'count users', count
137 | ```
138 |
139 | ## `count(property, values, callback)`
140 |
141 | Count the number of one or more values for an indexed property.
142 |
143 | Counting multiple values:
144 |
145 | ```coffee
146 | Users.get 'users', properties:
147 | user_id: identifier: true
148 | job: index: true
149 | Users.count 'job' [ 'globtrotter', 'icemaker' ], (err, counts) ->
150 | console.log 'count globtrotter', counts[0]
151 | console.log 'count icemaker', counts[1]
152 | ```
153 |
154 | count: (callback) ->
155 | {redis} = @
156 | {db, name, identifier, index} = @data
157 | if arguments.length is 3
158 | property = callback
159 | values = arguments[1]
160 | callback = arguments[2]
161 | return callback new Error "Property is not indexed" unless index[property]
162 | isArray = Array.isArray values
163 | values = [values] unless isArray
164 | multi = redis.multi()
165 | for value, i in values
166 | value = @hash value
167 | multi.scard "#{db}:#{name}_#{property}:#{value}"
168 | multi.exec (err, counts) ->
169 | return callback err if err
170 | callback null, if isArray then counts else counts[0]
171 | else
172 | @redis.scard "#{db}:#{name}_#{identifier}", (err, count) ->
173 | return callback err if err
174 | callback null, count
175 |
176 | ## `create(records, [options], callback)`
177 |
178 | Insert one or multiple record. The records must not already exists
179 | in the database or an error will be returned in the callback. Only
180 | the defined properties are inserted.
181 |
182 | The records passed to the function are returned in the callback enriched their new identifier property.
183 |
184 | `records` Record object or array of record objects.
185 |
186 | `options` Options properties include:
187 |
188 | * `identifiers` Return only the created identifiers instead of the records.
189 | * `validate` Validate the records.
190 | * `properties` Array of properties to be returned.
191 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
192 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
193 |
194 | `callback` Called on success or failure. Received parameters are:
195 |
196 | * `err` Error object if any.
197 | * `records` Records with their newly created identifier.
198 |
199 | Records are not validated, it is the responsability of the client program calling `create` to either
200 | call `validate` before calling `create` or to passs the `validate` options.
201 |
202 | create: (records, options, callback) ->
203 | if arguments.length is 2
204 | callback = options
205 | options = {}
206 | {redis, hash} = @
207 | {db, name, temporal, properties, identifier, index, unique} = @data
208 | isArray = Array.isArray records
209 | records = [records] unless isArray
210 | # Validate records
211 | if options.validate
212 | try @validate records, throw: true
213 | catch e then return callback e, (if isArray then records else records[0])
214 | # Persist
215 | @exists records, (err, recordIds) =>
216 | return callback err if err
217 | for recordId in recordIds
218 | return callback new Error "Record #{recordId} already exists" if recordId?
219 | multi = redis.multi()
220 | # Get current date once if schema is temporal
221 | date = new Date Date.now() if temporal?
222 | # Generate new identifiers
223 | @id records, (err, records) =>
224 | multi = redis.multi()
225 | for record, i in records
226 | # Enrich the record with a creation date
227 | record[temporal.creation] = date if temporal?.creation? and not record[temporal.creation]?
228 | # Enrich the record with a creation date
229 | record[temporal.modification] = date if temporal?.modification? and not record[temporal.modification]?
230 | # Register new identifier
231 | multi.sadd "#{db}:#{name}_#{identifier}", record[identifier]
232 | # Deal with Unique
233 | for property of unique
234 | multi.hset "#{db}:#{name}_#{property}", record[property], record[identifier] if record[property]
235 | # Deal with Index
236 | for property of index
237 | value = record[property]
238 | value = hash value
239 | multi.sadd "#{db}:#{name}_#{property}:#{value}", record[identifier]
240 | #multi.zadd "#{s.db}:#{s.name}_#{property}", 0, record[property]
241 | # Store the record
242 | r = {}
243 | for property, value of record
244 | # Insert only defined properties
245 | continue unless properties[property]
246 | # Filter null values
247 | r[property] = value if value?
248 | @serialize r
249 | multi.hmset "#{db}:#{name}:#{record[identifier]}", r
250 | multi.exec (err, results) =>
251 | return callback err if err
252 | for result in results
253 | return callback new Error 'Corrupted user database ' if result[0] is not "0"
254 | @unserialize records, options
255 | callback null, if isArray then records else records[0]
256 |
257 | ## `exists(records, callback)`
258 |
259 | Check if one or more record exist. The existence of a record is based on its
260 | id or any property defined as unique. The provided callback is called with
261 | an error or the records identifiers. The identifiers respect the same
262 | structure as the provided records argument. If a record does not exists,
263 | its associated return value is null.
264 |
265 | `records` Record object or array of record objects.
266 |
267 | `callback` Called on success or failure. Received parameters are:
268 |
269 | * `err` Error object if any.
270 | * `identifier` Record identifiers or null values.
271 |
272 | exists: (records, callback) ->
273 | {redis} = @
274 | {db, name, identifier, unique} = @data
275 | isArray = Array.isArray records
276 | records = [records] unless isArray
277 | multi = redis.multi()
278 | for record in records
279 | if typeof record is 'object'
280 | if record[identifier]?
281 | recordId = record[identifier]
282 | multi.hget "#{db}:#{name}:#{recordId}", identifier
283 | else
284 | for property of unique
285 | if record[property]?
286 | multi.hget "#{db}:#{name}_#{property}", record[property]
287 | else
288 | multi.hget "#{db}:#{name}:#{record}", identifier
289 | multi.exec (err, recordIds) =>
290 | return callback err if err
291 | @unserialize recordIds
292 | callback null, if isArray then recordIds else recordIds[0]
293 |
294 | ## `get(records, [options], callback)`
295 |
296 | Retrieve one or multiple records. Records that doesn't exists are returned as null. If
297 | options is an array, it is considered to be the list of properties to retrieve. By default,
298 | unless the `force` option is defined, only the properties not yet defined in the provided
299 | records are fetched from Redis.
300 |
301 | `options` All options are optional. Options properties include:
302 |
303 | * `properties` Array of properties to fetch, all properties unless defined.
304 | * `force` Force the retrieval of properties even if already present in the record objects.
305 | * `accept_null` Skip objects if they are provided as null.
306 | * `object` If `true`, return an object where keys are the identifier and value are the fetched records
307 |
308 | `callback` Called on success or failure. Received parameters are:
309 |
310 | * `err` Error object if the command failed.
311 | * `records` Object or array of object if command succeed. Objects are null if records are not found.
312 |
313 | get: (records, options, callback) ->
314 | if arguments.length is 2
315 | callback = options
316 | options = {}
317 | if Array.isArray options
318 | options = {properties: options}
319 | {redis} = @
320 | {db, name, identifier} = @data
321 | isArray = Array.isArray records
322 | records = [records] unless isArray
323 | # Quick exit for accept_null
324 | if options.accept_null? and not records.some((record) -> record isnt null)
325 | return callback null, if isArray then records else records[0]
326 | # Retrieve records identifiers
327 | @identify records, {object: true, accept_null: options.accept_null?}, (err, records) =>
328 | return callback err if err
329 | cmds = []
330 | records.forEach (record, i) ->
331 | # An error would have been thrown by id if record was null and accept_null wasn't provided
332 | return unless record?
333 | recordId = record[identifier]
334 | if recordId is null
335 | records[i] = null
336 | else if options.properties?.length
337 | options.properties.forEach (property) ->
338 | unless options.force or record[property]
339 | cmds.push ['hget', "#{db}:#{name}:#{recordId}", property, (err, value) ->
340 | record[property] = value
341 | ]
342 | else
343 | cmds.push ['hgetall', "#{db}:#{name}:#{recordId}", (err, values) ->
344 | for property, value of values
345 | record[property] = value
346 | ]
347 | # Check if the record really exists
348 | cmds.push ['exists', "#{db}:#{name}:#{recordId}", (err, exists) ->
349 | records[i] = null unless exists
350 | ]
351 | # No need to go further
352 | if cmds.length is 0
353 | return callback null, if isArray then records else records[0]
354 | multi = redis.multi cmds
355 | multi.exec (err, values) =>
356 | return callback err if err
357 | @unserialize records
358 | if options.object
359 | recordsByIds = {}
360 | for record in records
361 | recordsByIds[record[identifier]] = record
362 | callback null, recordsByIds
363 | else
364 | callback null, if isArray then records else records[0]
365 |
366 | ## `id(records, callback)`
367 |
368 | Generate new identifiers. The first arguments `records` may be the number
369 | of ids to generate, a record or an array of records.
370 |
371 | id: (records, callback) ->
372 | {redis} = @
373 | {db, name, identifier, unique} = @data
374 | if typeof records is 'number'
375 | incr = records
376 | isArray = true
377 | records = for i in [0...records] then null
378 | else
379 | isArray = Array.isArray records
380 | records = [records] unless isArray
381 | incr = 0
382 | for record in records then incr++ unless record[identifier]
383 | redis.incrby "#{db}:#{name}_incr", incr, (err, recordId) =>
384 | recordId = recordId - incr
385 | return callback err if err
386 | for record, i in records
387 | records[i] = record = {} unless record
388 | recordId++ unless record[identifier]
389 | # Enrich the record with its identifier
390 | record[identifier] = recordId unless record[identifier]
391 | callback null, if isArray then records else records[0]
392 |
393 | ## `identify(records, [options], callback)`
394 |
395 | Extract record identifiers or set the identifier to null if its associated record could not be found.
396 |
397 | The method doesn't hit the database to validate record values and if an id is
398 | provided, it won't check its existence. When a record has no identifier but a unique value, then its
399 | identifier will be fetched from Redis.
400 |
401 | `records` Record object or array of record objects.
402 |
403 | `options` Options properties include:
404 |
405 | * `accept_null` Skip objects if they are provided as null.
406 | * `object` Return an object in the callback even if it recieve an id instead of a record.
407 |
408 | Use reverse index lookup to extract user ids:
409 |
410 | ```coffee
411 | Users.get 'users', properties:
412 | user_id: identifier: true
413 | username: unique: true
414 | Users.id [
415 | {username: 'username_1'}
416 | {username: 'username_2'}
417 | ], (err, ids) ->
418 | should.not.exist err
419 | console.log ids
420 | ```
421 |
422 | Use the `object` option to return records instead of ids:
423 |
424 | ```coffee
425 | Users.get 'users', properties:
426 | user_id: identifier: true
427 | username: unique: true
428 | Users.id [
429 | 1, {user_id: 2} ,{username: 'username_3'}
430 | ], object: true, (err, users) ->
431 | should.not.exist err
432 | ids = for user in users then user.user_id
433 | console.log ids
434 | ```
435 |
436 | identify: (records, options, callback) ->
437 | if arguments.length is 2
438 | callback = options
439 | options = {}
440 | {redis} = @
441 | {db, name, identifier, unique} = @data
442 | isArray = Array.isArray records
443 | records = [records] unless isArray
444 | cmds = []
445 | err = null
446 | for record, i in records
447 | if typeof record is 'object'
448 | unless record?
449 | # Check if we allow records to be null
450 | unless options.accept_null
451 | return callback new Error 'Null record'
452 | else if record[identifier]?
453 | # It's perfect, no need to hit redis
454 | else
455 | withUnique = false
456 | for property of unique
457 | if record[property]?
458 | withUnique = true
459 | cmds.push ['hget', "#{db}:#{name}_#{property}", record[property], ((record) -> (err, recordId) ->
460 | record[identifier] = recordId
461 | )(record)]
462 | # Error if no identifier and no unique value provided
463 | return callback new Error 'Invalid record, got ' + (JSON.stringify record) unless withUnique
464 | else if typeof record is 'number' or typeof record is 'string'
465 | records[i] = {}
466 | records[i][identifier] = record
467 | else
468 | return callback new Error 'Invalid id, got ' + (JSON.stringify record)
469 | finalize = ->
470 | unless options.object
471 | records = for record in records
472 | if record? then record[identifier] else record
473 | callback null, if isArray then records else records[0]
474 | # No need to hit redis if there is no command
475 | return finalize() if cmds.length is 0
476 | # Run the commands
477 | multi = redis.multi cmds
478 | multi.exec (err, results) =>
479 | return callback err if err
480 | @unserialize records
481 | finalize()
482 |
483 | ## `list([options], callback)`
484 |
485 | List records with support for filtering and sorting.
486 |
487 | `options` Options properties include:
488 |
489 | * `direction` One of `asc` or `desc`, default to `asc`.
490 | * `identifiers` Return an array of identifiers instead of the record objects.
491 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
492 | * `properties` Array of properties to be returned.
493 | * `operation` Redis operation in case of multiple `where` properties, default to `union`.
494 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
495 | * `sort` Name of the property by which records should be ordered.
496 | * `where` Hash of property/value used to filter the query.
497 |
498 | `callback` Called on success or failure. Received parameters are:
499 |
500 | * `err` Error object if any.
501 | * `records` Records fetched from Redis.
502 |
503 | Using the `union` operation:
504 |
505 | ```coffee
506 | Users.list
507 | where: group: ['admin', 'redis']
508 | operation: 'union'
509 | direction: 'desc'
510 | , (err, users) ->
511 | console.log users
512 | ```
513 |
514 | An alternative syntax is to bypass the `where` option, the exemple above
515 | could be rewritten as:
516 |
517 | ```coffee
518 | Users.list
519 | group: ['admin', 'redis']
520 | operation: 'union'
521 | direction: 'desc'
522 | , (err, users) ->
523 | console.log users
524 | ```
525 |
526 | list: (options, callback) ->
527 | if typeof options is 'function'
528 | callback = options
529 | options = {}
530 | {redis, hash} = @
531 | {db, name, identifier, index} = @data
532 | options.properties = options.properties or Object.keys @data.properties
533 | options.properties = [identifier] if options.identifiers
534 | args = []
535 | multi = @redis.multi()
536 | # Index
537 | options.where = {} unless options.where?
538 | where = []
539 | for property, value of options
540 | if index[property]
541 | if Array.isArray value
542 | for v in value
543 | where.push [property, v]
544 | else
545 | where.push [property, value]
546 | options.where = if Object.keys(options.where).length then options.where else false
547 | if where.length is 1
548 | [property, value] = where[0]
549 | value = hash value
550 | args.push "#{db}:#{name}_#{property}:#{value}"
551 | else if where.length > 1
552 | tempkey = "temp:#{(new Date).getTime()}#{Math.random()}"
553 | keys = []
554 | keys.push tempkey
555 | args.push tempkey
556 | for filter in where
557 | [property, value] = filter
558 | value = hash value
559 | keys.push "#{db}:#{name}_#{property}:#{value}"
560 | operation = options.operation ? 'union'
561 | multi["s#{operation}store"] keys...
562 | else
563 | args.push "#{db}:#{name}_#{identifier}"
564 | # Sorting by one property
565 | if options.sort?
566 | args.push 'by'
567 | args.push "#{db}:#{name}:*->" + options.sort
568 | # Properties to return
569 | for property in options.properties
570 | args.push 'get'
571 | args.push "#{db}:#{name}:*->" + property
572 | # Sorting property is a string
573 | args.push 'alpha'
574 | # Sorting direction
575 | args.push options.direction ? 'asc'
576 | # Callback
577 | args.push (err, values) =>
578 | return callback err if err
579 | return callback null, [] unless values.length
580 | result = for i in [0 ... values.length] by options.properties.length
581 | record = {}
582 | for property, j in options.properties
583 | record[property] = values[i + j]
584 | @unserialize record, options
585 | callback null, result
586 | # Run command
587 | multi.sort args...
588 | multi.del tempkey if tempkey
589 | multi.exec()
590 |
591 | ## `remove(records, callback)`
592 |
593 | Remove one or several records from the database. The function will also
594 | handle all the indexes referencing those records.
595 |
596 | `records` Record object or array of record objects.
597 |
598 | `callback` Called on success or failure. Received parameters are:
599 |
600 | * `err` Error object if any.
601 | * `removed` Number of removed records.
602 |
603 | Removing a single record:
604 |
605 | ```coffee
606 | Users.remove id, (err, removed) ->
607 | console.log "#{removed} user removed"
608 | ```
609 |
610 | remove: (records, callback) ->
611 | {redis, hash} = @
612 | {db, name, identifier, index, unique} = @data
613 | isArray = Array.isArray records
614 | records = [records] unless isArray
615 | removed = 0
616 | @get records, [].concat(Object.keys(unique), Object.keys(index)), (err, records) ->
617 | return callback err if err
618 | multi = redis.multi()
619 | for record in records
620 | # Bypass null records, not much to do with it
621 | continue if record is null
622 | do (record) ->
623 | # delete objects
624 | recordId = record[identifier]
625 | multi.del "#{db}:#{name}:#{recordId}", (err) ->
626 | removed++
627 | # delete indexes
628 | multi.srem "#{db}:#{name}_#{identifier}", recordId
629 | for property of unique
630 | multi.hdel "#{db}:#{name}_#{property}", record[property]
631 | for property of index
632 | value = hash record[property]
633 | multi.srem "#{db}:#{name}_#{property}:#{value}", recordId, (err, count) ->
634 | console.warn('Missing indexed property') if count isnt 1
635 | multi.exec (err, results) ->
636 | return callback err if err
637 | callback null, removed
638 |
639 | ## `update(records, [options], callback)`
640 |
641 | Update one or several records. The records must exists in the database or
642 | an error will be returned in the callback. The existence of a record may
643 | be discovered through its identifier or the presence of a unique property.
644 |
645 | `records` Record object or array of record objects.
646 |
647 | `options` Options properties include:
648 |
649 | * `validate` Validate the records.
650 |
651 | `callback` Called on success or failure. Received parameters are:
652 |
653 | * `err` Error object if any.
654 | * `records` Records with their newly created identifier.
655 |
656 | Records are not validated, it is the responsability of the client program to either
657 | call `validate` before calling `update` or to passs the `validate` options.
658 |
659 | Updating a single record:
660 |
661 | ```coffee
662 | Users.update
663 | username: 'my_username'
664 | age: 28
665 | , (err, user) -> console.log user
666 | ```
667 |
668 | update: (records, options, callback) ->
669 | if arguments.length is 2
670 | callback = options
671 | options = {}
672 | {redis, hash} = @
673 | {db, name, temporal, properties, identifier, unique, index} = @data
674 | isArray = Array.isArray records
675 | records = [records] unless isArray
676 | # Validate records
677 | if options.validate
678 | try @validate records, {throw: true, skip_required: true}
679 | catch e then return callback e, (if isArray then records else records[0])
680 | # 1. Get values of indexed properties
681 | # 2. If indexed properties has changed
682 | # 2.1 Make sure the new property is not assigned to another record
683 | # 2.2 Erase old index & Create new index
684 | # 3. Save the record
685 | @identify records, {object: true}, (err, records) =>
686 | return callback err if err
687 | # Stop here if a record is invalid
688 | for record in records
689 | return callback new Error 'Invalid record' unless record
690 | # Find records with a possible updated index
691 | cmdsCheck = []
692 | cmdsUpdate = []
693 | multi = redis.multi()
694 | for record in records
695 | # Stop here if we couldn't get an id
696 | recordId = record[identifier]
697 | return callback new Error 'Unsaved record' unless recordId
698 | # Enrich the record with a modification date
699 | record[temporal.modification] = new Date Date.now() if temporal?.modification? and not record[temporal.modification]?
700 | r = {}
701 | # Filter null values
702 | for property, value of record
703 | if value?
704 | r[property] = value
705 | else
706 | cmdsUpdate.push ['hdel', "#{db}:#{name}:#{recordId}", property ]
707 | @serialize r
708 | cmdsUpdate.push ['hmset', "#{db}:#{name}:#{recordId}", r ]
709 | # If an index has changed, we need to update it
710 | do (record) ->
711 | recordId = record[identifier]
712 | potentiallyChangedProperties = []
713 | # Find the indexed properties that may have changed
714 | for property in [].concat(Object.keys(unique), Object.keys(index))
715 | potentiallyChangedProperties.push property if typeof record[property] isnt 'undefined'
716 | if potentiallyChangedProperties.length
717 | # Get the persisted value for those indexed properties
718 | multi.hmget "#{db}:#{name}:#{recordId}", potentiallyChangedProperties..., (err, values) ->
719 | for property, propertyI in potentiallyChangedProperties
720 | if values[propertyI] isnt record[property]
721 | if properties[property].unique
722 | # First we check if index for new key exists to avoid duplicates
723 | cmdsCheck.push ['hexists', "#{db}:#{name}_#{property}", record[property] ]
724 | # Second, if it exists, erase old key and set new one
725 | cmdsUpdate.push ['hdel', "#{db}:#{name}_#{property}", values[propertyI] ]
726 | cmdsUpdate.push ['hsetnx', "#{db}:#{name}_#{property}", record[property], recordId, (err, success) ->
727 | console.warn 'Trying to write on existing unique property' unless success
728 | ]
729 | else if properties[property].index
730 | valueOld = hash values[propertyI]
731 | valueNew = hash record[property]
732 | cmdsUpdate.push ['srem', "#{db}:#{name}_#{property}:#{valueOld}", recordId ]
733 | cmdsUpdate.push ['sadd', "#{db}:#{name}_#{property}:#{valueNew}", recordId ]
734 | # Get the value of those indexed properties to see if they changed
735 | multi.exec (err, values) ->
736 | # Check if unique properties doesn't already exists
737 | multi = redis.multi cmdsCheck
738 | multi.exec (err, exists) ->
739 | return callback err if err
740 | for exist in exists
741 | return callback new Error 'Unique value already exists' if exist isnt 0
742 | # Update properties
743 | multi = redis.multi cmdsUpdate
744 | multi.exec (err, results) ->
745 | return callback err if err
746 | callback null, if isArray then records else records[0]
747 |
--------------------------------------------------------------------------------
/src/Schema.coffee.md:
--------------------------------------------------------------------------------
1 |
2 | crypto = require 'crypto'
3 |
4 | isEmail = (email) ->
5 | /^[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+(\.[a-z0-9,!#\$%&'\*\+\/\=\?\^_`\{\|}~\-]+)*@[a-z0-9\-]+(\.[a-z0-9\-]+)*\.([a-z]{2,})$/.test(email)
6 |
7 | # Schema definition
8 |
9 | Schema is a mixin from which `Records` inherits. A schema is defined once
10 | and must no change. We dont support schema migration at the moment. The `Records`
11 | class inherit all the properties and method of the shema.
12 |
13 | `ron` Reference to the Ron instance
14 |
15 | `options` Schema definition. Options include:
16 |
17 | * `name` Name of the schema.
18 | * `properties` Properties definition, an object or an array.
19 |
20 | Record properties may be defined by the following keys:
21 |
22 | * `type` Use to cast the value inside Redis, one of `string`, `int`, `date` or `email`.
23 | * `identifier` Mark this property as the identifier, only one property may be an identifier.
24 | * `index` Create an index on the property.
25 | * `unique` Create a unique index on the property.
26 | * `temporal` Add creation and modification date transparently.
27 |
28 | Define a schema from a configuration object:
29 |
30 | ```coffee
31 | ron.get 'users', properties:
32 | user_id: identifier: true
33 | username: unique: true
34 | password: true
35 | ```
36 |
37 | Define a schema with a declarative approach:
38 |
39 | ```coffee
40 | Users = ron.get 'users'
41 | Users.indentifier 'user_id'
42 | Users.unique 'username'
43 | Users.property 'password'
44 | ```
45 |
46 | Whichever your style, you can then manipulate your records:
47 |
48 | ```coffee
49 | users = ron.get 'users'
50 | users.list (err, users) -> console.log users
51 | ```
52 |
53 | module.exports = class Schema
54 |
55 | constructor: (ron, options) ->
56 | @ron = ron
57 | options = {name: options} if typeof options is 'string'
58 | @data =
59 | db: ron.name
60 | name: options.name
61 | temporal: {}
62 | properties: {}
63 | identifier: null
64 | index: {}
65 | unique: {}
66 | if options.temporal
67 | @temporal options.temporal
68 | if options.properties
69 | for name, value of options.properties
70 | @property name, value
71 |
72 | ## `hash(key)`
73 |
74 | Utility function used when a redis key is created out of
75 | uncontrolled character (like a string instead of an int).
76 |
77 | hash: (key) ->
78 | key = "#{key}" if typeof key is 'number'
79 | return if key? then crypto.createHash('sha1').update(key).digest('hex') else 'null'
80 |
81 |
82 | ## `identifier(property)`
83 |
84 | Define a property as an identifier or return the record identifier if
85 | called without any arguments. An identifier is a property which uniquely
86 | define a record. Inside Redis, identifier values are stored in set.
87 |
88 | identifier: (property) ->
89 | # Set the property
90 | if property?
91 | @data.properties[property] = {} unless @data.properties[property]?
92 | @data.properties[property].type = 'int'
93 | @data.properties[property].identifier = true
94 | @data.identifier = property
95 | @
96 | # Get the property
97 | else
98 | @data.identifier
99 |
100 | ## `index([property])`
101 |
102 | Define a property as indexable or return all index properties. An
103 | indexed property allow records access by its property value. For example,
104 | when using the `list` function, the search can be filtered such as returned
105 | records match one or multiple values.
106 |
107 | Calling this function without any argument will return an array with all the
108 | indexed properties.
109 |
110 | Example:
111 |
112 | ```coffee
113 | User.index 'email'
114 | User.list { filter: { email: 'my@email.com' } }, (err, users) ->
115 | console.log 'This user has the following accounts:'
116 | for user in user
117 | console.log "- #{user.username}"
118 | ```
119 |
120 | index: (property) ->
121 | # Set the property
122 | if property?
123 | @data.properties[property] = {} unless @data.properties[property]?
124 | @data.properties[property].index = true
125 | @data.index[property] = true
126 | @
127 | # Get the property
128 | else
129 | Object.keys(@data.index)
130 |
131 | ## `property(property, [schema])`
132 |
133 | Define a new property or overwrite the definition of an
134 | existing property. If no schema is provide, return the
135 | property information.
136 |
137 | Calling this function with only the property argument will return the schema
138 | information associated with the property.
139 |
140 | It is possible to define a new property without any schema information by
141 | providing an empty object.
142 |
143 | Example:
144 | ```
145 | User.property 'id', identifier: true
146 | User.property 'username', unique: true
147 | User.property 'email', { index: true, type: 'email' }
148 | User.property 'name', {}
149 | ```
150 |
151 | property: (property, schema) ->
152 | if schema?
153 | schema ?= {}
154 | schema.name = property
155 | @data.properties[property] = schema
156 | @identifier property if schema.identifier
157 | @index property if schema.index
158 | @unique property if schema.unique
159 | @
160 | else
161 | @data.properties[property]
162 |
163 | ## `name()`
164 |
165 | Return the schema name of the current instance.
166 |
167 | Using the function :
168 |
169 | ```coffee
170 | Users = client 'users', properties: username: unique: true
171 | console.log Users.name() is 'users'
172 | ```
173 |
174 | name: -> @data.name
175 |
176 | ## `serialize(records)`
177 |
178 | Cast record values before their insertion into Redis.
179 |
180 | Take a record or an array of records and update values with correct
181 | property types.
182 |
183 | serialize: (records) ->
184 | {properties} = @data
185 | isArray = Array.isArray records
186 | records = [records] unless isArray
187 | for record, i in records
188 | continue unless record?
189 | # Convert the record
190 | if typeof record is 'object'
191 | for property, value of record
192 | if properties[property]?.type is 'date' and value?
193 | if typeof value is 'number'
194 | # its a timestamp
195 | else if typeof value is 'string'
196 | if /^\d+$/.test value
197 | record[property] = parseInt value, 10
198 | else
199 | record[property] = Date.parse value
200 | else if typeof value is 'object' and value instanceof Date
201 | record[property] = value.getTime()
202 | if isArray then records else records[0]
203 |
204 | ## `temporal([options])`
205 |
206 | Define or retrieve temporal definition. Marking a schema as
207 | temporal will transparently add two new date properties, the
208 | date when the record was created (by default "cdate"), and the date
209 | when the record was last updated (by default "mdate").
210 |
211 | temporal: (temporal) ->
212 | if temporal?
213 | if temporal is true
214 | temporal =
215 | creation: 'cdate'
216 | modification: 'mdate'
217 | @data.temporal = temporal
218 | @property temporal.creation, type: 'date'
219 | @property temporal.modification, type: 'date'
220 | else
221 | [ @data.temporal.creation, @data.temporal.modification ]
222 |
223 | ## `unique([property])`
224 |
225 | Define a property as unique or retrieve all the unique properties if no
226 | argument is provided. An unique property is similar to a index
227 | property but the index is stored inside a Redis hash. In addition to being
228 | filterable, it could also be used as an identifer to access a record.
229 |
230 | Example:
231 |
232 | ```coffee
233 | User.unique 'username'
234 | User.get { username: 'me' }, (err, user) ->
235 | console.log "This is #{user.username}"
236 | ```
237 |
238 | unique: (property) ->
239 | # Set the property
240 | if property?
241 | @data.properties[property] = {} unless @data.properties[property]?
242 | @data.properties[property].unique = true
243 | @data.unique[property] = true
244 | @
245 | # Get the property
246 | else
247 | Object.keys(@data.unique)
248 |
249 | ## `unserialize(records, [options])`
250 |
251 | Cast record values to their correct type.
252 |
253 | Take a record or an array of records and update values with correct
254 | property types.
255 |
256 | `options` Options include:
257 |
258 | * `identifiers` Return an array of identifiers instead of the record objects.
259 | * `properties` Array of properties to be returned.
260 | * `milliseconds` Convert date value to milliseconds timestamps instead of `Date` objects.
261 | * `seconds` Convert date value to seconds timestamps instead of `Date` objects.
262 |
263 | unserialize: (records, options = {}) ->
264 | {identifier, properties} = @data
265 | isArray = Array.isArray records
266 | records = [records] unless isArray
267 | options.properties = [identifier] if options.identifiers
268 | for record, i in records
269 | continue unless record?
270 | # Convert the record
271 | if typeof record is 'object'
272 | for property, value of record
273 | if options.properties and options.properties.indexOf(property) is -1
274 | delete record[property]
275 | continue
276 | if properties[property]?.type is 'int' and value?
277 | record[property] = parseInt value, 10
278 | else if properties[property]?.type is 'date' and value?
279 | if /^\d+$/.test value
280 | value = parseInt value, 10
281 | else
282 | value = Date.parse value
283 | if options.milliseconds
284 | record[property] = value
285 | else if options.seconds
286 | record[property] = Math.round( value / 1000 )
287 | else record[property] = new Date value
288 | records[i] = record[identifier] if options.identifiers
289 | # By convension, this has to be an identifier but we can't check it
290 | else if typeof record is 'number' or typeof record is 'string'
291 | records[i] = parseInt record
292 |
293 | if isArray then records else records[0]
294 |
295 | ## `validate(records, [options])`
296 |
297 | Validate the properties of one or more records. Return a validation
298 | object or an array of validation objects depending on the provided
299 | records arguments. Keys of a validation object are the name of the invalid
300 | properties and their value is a string indicating the type of error.
301 |
302 | `records` Record object or array of record objects.
303 |
304 | `options` Options include:
305 |
306 | * `throw` Throw errors on first invalid property instead of returning a validation object.
307 | * `skip_required` Doesn't validate missing properties defined as `required`, usefull for partial update.
308 |
309 | validate: (records, options = {}) ->
310 | {db, name, properties} = @data
311 | # console.log 'records', records
312 | isArray = Array.isArray records
313 | records = [records] unless isArray
314 | validations = for record in records
315 | validation = {}
316 | for x, property of properties
317 | if not options.skip_required and property.required and not record[property.name]?
318 | if options.throw
319 | then throw new Error "Required property #{property.name}"
320 | else validation[property.name] = 'required'
321 | else if property.type is 'email' and not isEmail record[property.name]
322 | if options.throw
323 | then throw new Error "Invalid email #{record[property.name]}"
324 | else validation[property.name] = 'invalid_email'
325 | validation
326 | if isArray then validations else validations[0]
327 |
--------------------------------------------------------------------------------
/src/doc.coffee:
--------------------------------------------------------------------------------
1 |
2 | fs = require 'fs'
3 | mecano = require 'mecano'
4 | each = require 'each'
5 |
6 | date = -> d = (new Date).toISOString()
7 |
8 | convert_anchor = (text) ->
9 | re_anchor = /`([\w]+)\(/g
10 | text.replace re_anchor, (str, code) ->
11 | # At least in FF, doesn't close the tag
12 | "\n`#{code}("
13 |
14 | convert_code = (text) ->
15 | re_code = /\n(\s{2}\s*?\w[\s\S]*?)\n(?!\s)/g
16 | text.replace re_code, (str, code) ->
17 | code = code.split('\n').map((line)->line.substr(4)).join('\n')
18 | "\n```coffeescript\n#{code}\n```\n"
19 |
20 | each( ['Client', 'Schema', 'Records'] )
21 | .parallel( true )
22 | .on 'item', (file, next) ->
23 | source = "#{__dirname}/#{file}.coffee"
24 | destination = "#{__dirname}/../doc/#{file.toLowerCase()}.md"
25 | fs.readFile source, 'ascii', (err, content) ->
26 | return console.error err if err
27 | re = /###\n([\s\S]*?)\n( *)###/g
28 | re_title = /([\s\S]+)\n={2}=+([\s\S]*)/g
29 | match = re.exec content
30 | # docs += match[1]
31 | match = re_title.exec match[1]
32 | docs = """
33 | ---
34 | language: en
35 | layout: page
36 | title: "#{match[1]}"
37 | date: #{date()}
38 | comments: false
39 | sharing: false
40 | footer: false
41 | navigation: ron
42 | github: https://github.com/wdavidw/node-ron
43 | ---
44 | #{convert_code match[2]}
45 | """
46 | while match = re.exec content
47 | # Unindent
48 | match[1] = match[1].split('\n').map((line)->line.substr(2)).join('\n')
49 | docs += convert_code convert_anchor match[1]
50 | docs += '\n'
51 | fs.writeFile destination, docs, next
52 | .on 'both', (err) ->
53 | return console.error err if err
54 | console.log 'Documentation generated'
55 | destination = process.argv[2]
56 | return unless destination
57 | each( ['index', 'client', 'schema', 'records'] )
58 | .on 'item', (next, file) ->
59 | mecano.copy
60 | source: "#{__dirname}/../doc/#{file}.md"
61 | destination: destination
62 | force: true
63 | , next
64 | .on 'both', (err) ->
65 | return console.error err if err
66 | console.log 'Documentation published'
67 |
68 |
--------------------------------------------------------------------------------
/src/index.coffee:
--------------------------------------------------------------------------------
1 |
2 | Client = require './Client'
3 |
4 | module.exports = (options) ->
5 | new Client options
6 |
7 | module.exports.Client = Client
8 |
9 | module.exports.Records = require './Records'
10 |
11 | module.exports.Schema = require './Schema'
12 |
--------------------------------------------------------------------------------
/test/all.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | next()
18 |
19 | beforeEach (next) ->
20 | Users.clear next
21 |
22 | afterEach (next) ->
23 | client.redis.keys '*', (err, keys) ->
24 | should.not.exists err
25 | keys.should.eql []
26 | next()
27 |
28 | after (next) ->
29 |
30 | client.quit next
31 |
32 | describe 'all', ->
33 |
34 | it 'shall create 2 users and list them', (next) ->
35 | Users.create [
36 | username: 'my_username_1',
37 | email: 'my_first@email.com'
38 | ,
39 | username: 'my_username_2',
40 | email: 'my_second@email.com'
41 | ], (err, users) ->
42 | Users.all (err, users) ->
43 | should.not.exist err
44 | users.length.should.eql 2
45 | for user in users then user.username.should.match /^my_/
46 | Users.clear next
47 |
48 |
--------------------------------------------------------------------------------
/test/clear.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | describe 'all', ->
8 |
9 | client = Users = null
10 |
11 | before (next) ->
12 | client = ron config
13 | Users = client.get
14 | name: 'users'
15 | properties:
16 | user_id: identifier: true
17 | username: unique: true
18 | email: index: true
19 | next()
20 |
21 | beforeEach (next) ->
22 | Users.clear next
23 |
24 | afterEach (next) ->
25 | client.redis.keys '*', (err, keys) ->
26 | should.not.exists err
27 | keys.should.eql []
28 | next()
29 |
30 | after (next) ->
31 | client.quit next
32 |
33 | it 'shall clear nothing if there are no record', (next) ->
34 | Users.all (err, users) ->
35 | users.length.should.equal 0
36 | Users.clear (err, count) ->
37 | should.not.exist err
38 | count.should.eql 0
39 | next()
40 |
41 | it 'shall clear simple records', (next) ->
42 | Records = client.get { name: 'records', properties: id_records: identifier: true }
43 | Records.all (err, users) ->
44 | users.length.should.equal 0
45 | Records.clear (err, count) ->
46 | should.not.exist err
47 | count.should.eql 0
48 | Records.create {}, (err, count) ->
49 | should.not.exist err
50 | Records.clear (err, count) ->
51 | should.not.exist err
52 | count.should.eql 1
53 | next()
54 |
55 |
--------------------------------------------------------------------------------
/test/client_get.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | describe 'client get', ->
8 |
9 | it 'should accept a string name', (next) ->
10 | client = ron config
11 | Users = client.get 'users'
12 | Users.should.be.instanceof ron.Schema
13 | Users.should.be.instanceof ron.Records
14 | client.quit next
15 |
16 | it 'should accept an object with a name', (next) ->
17 | client = ron config
18 | Users = client.get name: 'users'
19 | Users.should.be.instanceof ron.Schema
20 | Users.should.be.instanceof ron.Records
21 | client.quit next
22 |
23 | it 'should mix a string name and an object', (next) ->
24 | client = ron config
25 | validate = (schema) ->
26 | schema.name().should.equal 'users'
27 | schema.property('username').should.eql { name: 'username', unique: true }
28 | validate client.get 'users', temporal: true, properties:
29 | username: unique: true
30 | validate client.get 'users'
31 | client.quit next
32 |
33 | it 'should define and retrieve an identifier', (next) ->
34 | client = ron config
35 | Users = client.get name: 'users'
36 | Users.should.eql Users.identifier('id')
37 | Users.identifier().should.eql 'id'
38 | client.quit next
39 |
40 | it 'should define and retrieve an index', (next) ->
41 | client = ron config
42 | Users = client.get name: 'users'
43 | Users.should.eql Users.index('my_index')
44 | ['my_index'].should.eql Users.index()
45 | client.quit next
46 |
47 | it 'should define and retrieve a unique index', (next) ->
48 | client = ron config
49 | Users = client.get name: 'users'
50 | Users.should.eql Users.unique('my_unique')
51 | Users.unique().should.eql ['my_unique']
52 | client.quit next
53 |
54 | it 'should define multiple properties from object', (next) ->
55 | client = ron config
56 | # Define properties
57 | Users = client.get
58 | name: 'users'
59 | properties:
60 | id: identifier: true
61 | username: unique: true
62 | email: { type: 'email', index: true }
63 | name: {}
64 | # Retrieve properties
65 | Users.property('id').should.eql { name: 'id', identifier: true, type: 'int' }
66 | Users.property('username').should.eql { name: 'username', unique: true }
67 | Users.property('email').should.eql { name: 'email', index: true, type: 'email' }
68 | Users.property('name').should.eql { name: 'name' }
69 | client.quit next
70 |
71 | it 'should define multiple properties by calling functions', (next) ->
72 | client = ron config
73 | Users = client.get name: 'users'
74 | # Define properties
75 | Users.should.eql Users.property('id', identifier: true)
76 | Users.should.eql Users.property('username', unique: true)
77 | Users.should.eql Users.property('email', { index: true, type: 'email' })
78 | Users.should.eql Users.property('name', {})
79 | # Retrieve properties
80 | Users.property('id').should.eql { name: 'id', identifier: true, type: 'int' }
81 | Users.property('username').should.eql { name: 'username', unique: true }
82 | Users.property('email').should.eql { name: 'email', index: true, type: 'email' }
83 | Users.property('name').should.eql { name: 'name' }
84 | client.quit next
85 |
86 |
87 |
--------------------------------------------------------------------------------
/test/count.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 |
8 | client = Users = null
9 |
10 | before (next) ->
11 | client = ron config
12 | Users = client.get 'users'
13 | Users.identifier 'user_id'
14 | Users.unique 'username'
15 | Users.index 'email'
16 | next()
17 |
18 | beforeEach (next) ->
19 | Users.clear next
20 |
21 | afterEach (next) ->
22 | client.redis.keys '*', (err, keys) ->
23 | should.not.exists err
24 | keys.should.eql []
25 | next()
26 |
27 | after (next) ->
28 | client.quit next
29 |
30 | describe 'count', ->
31 |
32 | it 'should count the records', (next) ->
33 | Users.create [
34 | username: '1my_username',
35 | email: '1my@email.com',
36 | password: 'my_password'
37 | ,
38 | username: '2my_username',
39 | email: '2my@email.com',
40 | password: 'my_password'
41 | ], (err, user) ->
42 | Users.count (err, count) ->
43 | should.not.exist err
44 | count.should.eql 2
45 | Users.clear next
46 |
47 | it 'should count the index elements of a property', (next) ->
48 | Users.create [
49 | username: 'username_1',
50 | email: 'my@email.com',
51 | password: 'my_password'
52 | ,
53 | username: 'username_2',
54 | email: 'my_2@email.com',
55 | password: 'my_password'
56 | ,
57 | username: 'username_3',
58 | email: 'my@email.com',
59 | password: 'my_password'
60 | ], (err, user) ->
61 | # Count one value
62 | Users.count 'email', 'my@email.com', (err, count) ->
63 | should.not.exist err
64 | count.should.eql 2
65 | # Count multiple values
66 | Users.count 'email', ['my@email.com', 'my_2@email.com'], (err, counts) ->
67 | should.not.exist err
68 | counts[0].should.eql 2
69 | counts[1].should.eql 1
70 | Users.clear next
71 |
--------------------------------------------------------------------------------
/test/create.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | next()
18 |
19 | beforeEach (next) ->
20 | Users.clear next
21 |
22 | afterEach (next) ->
23 | client.redis.keys '*', (err, keys) ->
24 | should.not.exists err
25 | keys.should.eql []
26 | next()
27 |
28 | after (next) ->
29 | client.quit next
30 |
31 | describe 'create', ->
32 |
33 | it 'Test create # one user', (next) ->
34 | Users.create
35 | username: 'my_username'
36 | email: 'my@email.com'
37 | password: 'my_password'
38 | , (err, user) ->
39 | should.not.exist err
40 | user.user_id.should.be.a.Number()
41 | user.email.should.eql 'my@email.com'
42 | # toto: Replace by User.remove
43 | Users.clear next
44 |
45 | it 'Test create # multiple users', (next) ->
46 | Users.create [
47 | username: 'my_username_1'
48 | email: 'my_first@email.com'
49 | password: 'my_password'
50 | ,
51 | username: 'my_username_2'
52 | email: 'my_second@email.com'
53 | password: 'my_password'
54 | ], (err, users) ->
55 | should.not.exist err
56 | users.length.should.eql 2
57 | users[0].password.should.eql 'my_password'
58 | # toto: Replace by Users.remove
59 | Users.clear next
60 |
61 | it 'Test create # existing id', (next) ->
62 | Users.create
63 | username: 'my_username'
64 | email: 'my@email.com'
65 | password: 'my_password'
66 | , (err, user) ->
67 | should.not.exist err
68 | Users.create {
69 | user_id: user.user_id,
70 | username: 'my_new_username',
71 | email: 'my_new@email.com',
72 | password: 'my_password'
73 | }, (err, user) ->
74 | err.message.should.eql 'Record 1 already exists'
75 | Users.clear next
76 |
77 | it 'Test create # unique exists', (next) ->
78 | Users.create
79 | username: 'my_username',
80 | email: 'my@email.com',
81 | password: 'my_password'
82 | , (err, user) ->
83 | should.not.exist err
84 | Users.create
85 | username: 'my_username'
86 | email: 'my@email.com'
87 | password: 'my_password'
88 | , (err, user) ->
89 | err.message.should.eql 'Record 1 already exists'
90 | Users.clear next
91 |
92 | it 'should only return the newly created identifiers', (next) ->
93 | Users.create [
94 | username: 'my_username_1'
95 | email: 'my_first@email.com'
96 | password: 'my_password'
97 | ,
98 | username: 'my_username_2'
99 | email: 'my_second@email.com'
100 | password: 'my_password'
101 | ], identifiers: true, (err, ids) ->
102 | should.not.exist err
103 | ids.length.should.equal 2
104 | for id in ids then id.should.be.a.Number()
105 | Users.clear next
106 |
107 | it 'should only return selected properties', (next) ->
108 | Users.create [
109 | username: 'my_username_1'
110 | email: 'my_first@email.com'
111 | password: 'my_password'
112 | ,
113 | username: 'my_username_2'
114 | email: 'my_second@email.com'
115 | password: 'my_password'
116 | ], properties: ['email'], (err, users) ->
117 | should.not.exist err
118 | users.length.should.equal 2
119 | for user in users then Object.keys(user).should.eql ['email']
120 | Users.clear next
121 |
122 | it 'should only insert defined properties', (next) ->
123 | Users.create
124 | username: 'my_username_1'
125 | email: 'my_first@email.com'
126 | zombie: 'e.t. maison'
127 | , (err, user) ->
128 | Users.get user.user_id, (err, user) ->
129 | should.not.exist user.zombie
130 | Users.clear next
131 |
132 | it 'should let you pass your own identifiers', (next) ->
133 | Users.create
134 | user_id: 1
135 | username: 'my_username_1'
136 | , (err, user) ->
137 | Users.get 1, (err, user) ->
138 | user.username.should.equal 'my_username_1'
139 | Users.clear next
140 |
--------------------------------------------------------------------------------
/test/exists.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | next()
18 |
19 | beforeEach (next) ->
20 | Users.clear next
21 |
22 | afterEach (next) ->
23 | client.redis.keys '*', (err, keys) ->
24 | should.not.exists err
25 | keys.should.eql []
26 | next()
27 |
28 | after (next) ->
29 | client.quit next
30 |
31 | describe 'exists', ->
32 |
33 | create = (callback) ->
34 | Users.create [
35 | username: 'my_username_1',
36 | email: 'my_1@email.com',
37 | password: 'my_password'
38 | ,
39 | username: 'my_username_2',
40 | email: 'my_2@email.com',
41 | password: 'my_password'
42 | ], (err, users) ->
43 | should.ifError err
44 | callback null, users
45 |
46 | it 'Test exists # true # identifier', (next) ->
47 | create (err, users) ->
48 | user = users[1]
49 | Users.exists user.user_id, (err, userId) ->
50 | should.not.exist err
51 | userId.should.eql user.user_id
52 | Users.clear next
53 |
54 | it 'Test exists # true # record with identifier', (next) ->
55 | create (err, users) ->
56 | user = users[1]
57 | Users.exists {user_id: user.user_id}, (err, userId) ->
58 | should.not.exist err
59 | userId.should.eql user.user_id
60 | Users.clear next
61 |
62 | it 'Test exists # true # record with unique property stored in hash', (next) ->
63 | create (err, users) ->
64 | user = users[1]
65 | Users.exists {username: user.username}, (err, userId) ->
66 | should.not.exist err
67 | userId.should.eql user.user_id
68 | Users.clear next
69 |
70 | it 'Test exists # false # indentifier', (next) ->
71 | Users.exists 'missing', (err, exists) ->
72 | should.not.exist err
73 | should.not.exist exists
74 | Users.clear next
75 |
76 | it 'Test exists # false # record with identifier', (next) ->
77 | Users.exists {user_id: 'missing'}, (err, exists) ->
78 | should.not.exist err
79 | should.not.exist exists
80 | Users.clear next
81 |
82 | it 'Test exists # false # record with unique property stored in hash', (next) ->
83 | Users.exists {username: 'missing'}, (err, exists) ->
84 | should.not.exist err
85 | should.not.exist exists
86 | Users.clear next
87 |
--------------------------------------------------------------------------------
/test/get.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | next()
18 |
19 | beforeEach (next) ->
20 | Users.clear next
21 |
22 | afterEach (next) ->
23 | client.redis.keys '*', (err, keys) ->
24 | should.not.exists err
25 | keys.should.eql []
26 | next()
27 |
28 | after (next) ->
29 | client.quit next
30 |
31 | describe 'get', ->
32 |
33 | it 'should use a provided identifier', (next) ->
34 | Users.create
35 | username: 'my_username'
36 | email: 'my@email.com'
37 | , (err, user) ->
38 | userId = user.user_id
39 | # Test with a number
40 | Users.get userId, (err, user) ->
41 | should.not.exist err
42 | user.user_id.should.eql userId
43 | user.username.should.eql 'my_username'
44 | user.email.should.eql 'my@email.com'
45 | # Test with a string
46 | Users.get '' + userId, (err, user) ->
47 | should.not.exist err
48 | user.user_id.should.eql userId
49 | Users.clear next
50 |
51 | it 'should faild with a missing identifier', (next) ->
52 | Users.get -1, (err, user) ->
53 | should.not.exist err
54 | should.not.exist user
55 | Users.clear next
56 |
57 | it 'Test get # unique property', (next) ->
58 | Users.create
59 | username: 'my_username'
60 | email: 'my@email.com'
61 | , (err, user) ->
62 | userId = user.user_id
63 | Users.get {username: 'my_username'}, (err, user) ->
64 | should.not.exist err
65 | user.user_id.should.eql userId
66 | Users.clear next
67 |
68 | it 'Test get # unique property missing', (next) ->
69 | Users.get {username: 'my_missing_username'}, (err, user) ->
70 | should.not.exist err
71 | should.not.exist user
72 | Users.clear next
73 |
74 | it 'should only return the provided properties', (next) ->
75 | Users.create
76 | username: 'my_username'
77 | email: 'my@email.com'
78 | , (err, user) ->
79 | userId = user.user_id
80 | Users.get userId, ['username'], (err, user) ->
81 | should.not.exist err
82 | user.user_id.should.eql userId
83 | user.username.should.eql 'my_username'
84 | should.not.exist user.email
85 | Users.clear next
86 |
87 | it 'should be able to get null records with option accept_null', (next) ->
88 | Users.create
89 | username: 'my_username',
90 | email: 'my@email.com',
91 | , (err, user) ->
92 | userId = user.user_id
93 | # A single null record
94 | Users.get null, accept_null: true, (err, user) ->
95 | should.not.exist err
96 | should.not.exist user
97 | # Multiple all null records
98 | Users.get [null, null], accept_null: true, (err, users) ->
99 | should.not.exist err
100 | users.length.should.eql 2
101 | for user in users then should.not.exist user
102 | # Multiple with null records
103 | Users.get [null, userId, null], accept_null: true, (err, users) ->
104 | should.not.exist err
105 | users.length.should.eql 3
106 | should.not.exist users[0]
107 | users[1].username.should.eql 'my_username'
108 | should.not.exist users[2]
109 | Users.clear next
110 |
111 | it 'should return an object where keys are the identifiers with option `object`', (next) ->
112 | Users.create [
113 | username: 'username_1'
114 | ,
115 | username: 'username_2'
116 | ,
117 | username: 'username_3'
118 | ], identifiers: true, (err, ids) ->
119 | Users.get ids, object: true, (err, users) ->
120 | Object.keys(users).length.should.eql 3
121 | for id, user of users then id.should.eql "#{user.user_id}"
122 | Users.clear next
123 |
--------------------------------------------------------------------------------
/test/id.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | next()
16 |
17 | beforeEach (next) ->
18 | Users.clear next
19 |
20 | afterEach (next) ->
21 | client.redis.keys '*', (err, keys) ->
22 | should.not.exists err
23 | keys.should.eql []
24 | next()
25 |
26 | after (next) ->
27 | client.quit next
28 |
29 | describe 'id', ->
30 |
31 | it "should take a number as the first argument", (next) ->
32 | Users.id 2, (err, users) ->
33 | users.length.should.eql 2
34 | ids = for user in users then user.user_id
35 | ids[0].should.eql ids[1] - 1
36 | Users.clear next
37 |
38 | it "should increment ids by default", (next) ->
39 | Users.id [{},{}], (err, users) ->
40 | users.length.should.eql 2
41 | ids = for user in users then user.user_id
42 | ids[0].should.eql ids[1] - 1
43 | Users.clear next
44 |
--------------------------------------------------------------------------------
/test/identify.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | next()
18 |
19 | beforeEach (next) ->
20 | Users.clear next
21 |
22 | afterEach (next) ->
23 | client.redis.keys '*', (err, keys) ->
24 | should.not.exists err
25 | keys.should.eql []
26 | next()
27 |
28 | after (next) ->
29 | client.quit next
30 |
31 | describe 'id', ->
32 |
33 | it 'Test id # number', (next) ->
34 | Users.identify 3, (err, userId) ->
35 | should.not.exist err
36 | userId.should.eql 3
37 | Users.identify [3], (err, userId) ->
38 | should.not.exist err
39 | userId.should.eql [3]
40 | Users.clear next
41 |
42 | it 'Test id # user.user_id', (next) ->
43 | Users.identify {user_id: 3}, (err, userId) ->
44 | should.not.exist err
45 | userId.should.eql 3
46 | Users.identify [{user_id: 3, username: 'my_username'}], (err, userId) ->
47 | should.not.exist err
48 | userId.should.eql [3]
49 | Users.clear next
50 |
51 | it 'Test id # user.username', (next) ->
52 | Users.create
53 | username: 'my_username',
54 | email: 'my@email.com',
55 | password: 'my_password'
56 | , (err, user) ->
57 | # Pass an object
58 | Users.identify {username: 'my_username'}, (err, userId) ->
59 | should.not.exist err
60 | userId.should.eql user.user_id
61 | # Pass an array of ids and objects
62 | Users.identify [1, {username: 'my_username'}, 2], (err, userId) ->
63 | should.not.exist err
64 | userId.should.eql [1, user.user_id, 2]
65 | Users.clear next
66 |
67 | it 'Test id # invalid object empty', (next) ->
68 | # Test an array of 3 arguments,
69 | # but the second is invalid since it's an empty object
70 | Users.identify [1, {}, {user_id: 2}], (err, user) ->
71 | err.message.should.eql 'Invalid record, got {}'
72 | Users.identify {}, (err, user) ->
73 | err.message.should.eql 'Invalid record, got {}'
74 | Users.clear next
75 |
76 | it 'Test id # missing unique', (next) ->
77 | # Test an array of 3 arguments,
78 | # but the second is invalid since it's an empty object
79 | Users.create [
80 | { username: 'my_username_1', email: 'my1@mail.com' }
81 | { username: 'my_username_2', email: 'my2@mail.com' }
82 | ], (err, users) ->
83 | # Test return id
84 | Users.identify [
85 | { username: users[1].username } # By unique
86 | { user_id: users[0].user_id } # By identifier
87 | { username: 'who are you' } # Alien
88 | ], (err, result) ->
89 | result[0].should.eql users[1].user_id
90 | result[1].should.eql users[0].user_id
91 | should.not.exist result[2]
92 | Users.clear next
93 |
94 | it 'Test id # missing unique + option object', (next) ->
95 | # Test an array of 3 arguments,
96 | # but the second is invalid since it's an empty object
97 | Users.create [
98 | { username: 'my_username_1', email: 'my1@mail.com' }
99 | { username: 'my_username_2', email: 'my2@mail.com' }
100 | ], (err, users) ->
101 | Users.identify [
102 | { username: users[1].username } # By unique
103 | { user_id: users[0].user_id } # By identifier
104 | { username: 'who are you' } # Alien
105 | ], {object: true}, (err, result) ->
106 | # Test return object
107 | result[0].user_id.should.eql users[1].user_id
108 | result[1].user_id.should.eql users[0].user_id
109 | should.not.exist result[2].user_id
110 | Users.clear next
111 |
112 | it 'Test id # invalid type id', (next) ->
113 | # Test an array of 3 arguments,
114 | # but the second is invalid since it's a boolean
115 | Users.identify [1, true, {user_id: 2}], (err, user) ->
116 | err.message.should.eql 'Invalid id, got true'
117 | Users.identify false, (err, user) ->
118 | err.message.should.eql 'Invalid id, got false'
119 | Users.clear next
120 |
121 | it 'Test id # invalid type null', (next) ->
122 | # Test an array of 3 arguments,
123 | # but the second is invalid since it's a boolean
124 | Users.identify [1, null, {user_id: 2}], (err, users) ->
125 | err.message.should.eql 'Null record'
126 | Users.identify null, (err, user) ->
127 | err.message.should.eql 'Null record'
128 | Users.clear next
129 |
130 | it 'Test id # accept null', (next) ->
131 | # Test an array of 3 arguments,
132 | # but the second is invalid since it's a boolean
133 | Users.identify [1, null, {user_id: 2}], {accept_null: true}, (err, users) ->
134 | should.not.exist err
135 | users.length.should.eql 3
136 | should.exist users[0]
137 | should.not.exist users[1]
138 | should.exist users[2]
139 | # Test null
140 | Users.identify null, {accept_null: true}, (err, user) ->
141 | should.not.exist err
142 | should.not.exist user
143 | Users.clear next
144 |
145 | it 'Test id # accept null return object', (next) ->
146 | # Same test than 'Test id # accept null' with the 'object' option
147 | Users.identify [1, null, {user_id: 2}], {accept_null: true, object: true}, (err, users) ->
148 | should.not.exist err
149 | users.length.should.eql 3
150 | users[0].user_id.should.eql 1
151 | should.not.exist users[1]
152 | users[2].user_id.should.eql 2
153 | # Test null
154 | Users.identify null, {accept_null: true, object: true}, (err, user) ->
155 | should.not.exist err
156 | should.not.exist user
157 | Users.clear next
158 |
159 | it 'Test id # id return object', (next) ->
160 | Users.create {
161 | username: 'my_username'
162 | email: 'my@email.com'
163 | password: 'my_password'
164 | }, (err, orgUser) ->
165 | # Pass an id
166 | Users.identify orgUser.user_id, {object: true}, (err, user) ->
167 | should.not.exist err
168 | user.should.eql {user_id: orgUser.user_id}
169 | # Pass an array of ids
170 | Users.identify [orgUser.user_id, orgUser.user_id], {object: true}, (err, user) ->
171 | user.should.eql [{user_id: orgUser.user_id}, {user_id: orgUser.user_id}]
172 | Users.clear next
173 |
174 | it 'Test id # unique + option object', (next) ->
175 | Users.create {
176 | username: 'my_username'
177 | email: 'my@email.com'
178 | password: 'my_password'
179 | }, (err, orgUser) ->
180 | # Pass an object
181 | Users.identify {username: 'my_username'}, {object: true}, (err, user) ->
182 | should.not.exist err
183 | user.should.eql {username: 'my_username', user_id: orgUser.user_id}
184 | # Pass an array of ids and objects
185 | Users.identify [1, {username: 'my_username'}, 2], {object: true}, (err, user) ->
186 | should.not.exist err
187 | user.should.eql [{user_id: 1}, {username: 'my_username', user_id: orgUser.user_id}, {user_id: 2}]
188 | Users.clear next
189 |
--------------------------------------------------------------------------------
/test/list.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | username: unique: true
16 | email: index: true
17 | name: index: true
18 | next()
19 |
20 | beforeEach (next) ->
21 | Users.clear next
22 |
23 | afterEach (next) ->
24 | client.redis.keys '*', (err, keys) ->
25 | should.not.exists err
26 | keys.should.eql []
27 | next()
28 |
29 | after (next) ->
30 | client.quit next
31 |
32 | describe 'list', ->
33 |
34 | it 'should be empty if there are no record', (next) ->
35 | Users.list { }, (err, users) ->
36 | should.not.exist err
37 | users.length.should.eql 0
38 | Users.clear next
39 |
40 | it 'should sort record according to one property', (next) ->
41 | Users.create [
42 | { username: 'username_1', email: '1@email.com', password: 'my_password' }
43 | { username: 'username_2', email: '2@email.com', password: 'my_password' }
44 | ], (err, users) ->
45 | Users.list { sort: 'username', direction: 'desc' }, (err, users) ->
46 | should.not.exist err
47 | users.length.should.eql 2
48 | users[0].username.should.eql 'username_2'
49 | users[1].username.should.eql 'username_1'
50 | Users.clear next
51 |
52 | it 'Test list # where', (next) ->
53 | Users.create [
54 | { username: 'username_1', email: '1@email.com', password: 'my_password' }
55 | { username: 'username_2', email: '2@email.com', password: 'my_password' }
56 | { username: 'username_3', email: '1@email.com', password: 'my_password' }
57 | ], (err, users) ->
58 | Users.list { email: '1@email.com', direction: 'desc' }, (err, users) ->
59 | should.not.exist err
60 | users.length.should.eql 2
61 | users[0].username.should.eql 'username_3'
62 | users[1].username.should.eql 'username_1'
63 | Users.clear next
64 |
65 | it 'Test list # where union, same property', (next) ->
66 | Users.create [
67 | { username: 'username_1', email: '1@email.com', password: 'my_password' }
68 | { username: 'username_2', email: '2@email.com', password: 'my_password' }
69 | { username: 'username_3', email: '1@email.com', password: 'my_password' }
70 | { username: 'username_4', email: '4@email.com', password: 'my_password' }
71 | ], (err, users) ->
72 | Users.list { email: ['1@email.com', '4@email.com'], operation: 'union', direction: 'desc' }, (err, users) ->
73 | should.not.exist err
74 | users.length.should.eql 3
75 | users[0].username.should.eql 'username_4'
76 | users[1].username.should.eql 'username_3'
77 | users[2].username.should.eql 'username_1'
78 | Users.clear next
79 |
80 | it 'should honor inter operation with a property having the same values', (next) ->
81 | Users.create [
82 | { username: 'username_1', email: '1@email.com', password: 'my_password', name: 'name_1' }
83 | { username: 'username_2', email: '2@email.com', password: 'my_password', name: 'name_2' }
84 | { username: 'username_3', email: '1@email.com', password: 'my_password', name: 'name_3' }
85 | { username: 'username_4', email: '4@email.com', password: 'my_password', name: 'name_4' }
86 | ], (err, users) ->
87 | Users.list { email: '1@email.com', name: 'name_3', operation: 'inter', direction: 'desc' }, (err, users) ->
88 | should.not.exist err
89 | users.length.should.eql 1
90 | users[0].username.should.eql 'username_3'
91 | Users.clear next
92 |
93 | it 'should return only selected properties', (next) ->
94 | Users.create [
95 | { username: 'username_1', email: '1@email.com', password: 'my_password', name: 'name_1' }
96 | { username: 'username_2', email: '2@email.com', password: 'my_password', name: 'name_2' }
97 | ], (err, users) ->
98 | Users.list { properties: ['username'], sort: 'username', direction: 'desc' }, (err, users) ->
99 | should.not.exist err
100 | users.length.should.eql 2
101 | for user in users then Object.keys(user).should.eql ['username']
102 | users[0].username.should.eql 'username_2'
103 | users[1].username.should.eql 'username_1'
104 | Users.clear next
105 |
106 | it 'should return an array of identifiers', (next) ->
107 | Users.create [
108 | { username: 'username_1', email: '1@email.com', password: 'my_password' }
109 | { username: 'username_2', email: '2@email.com', password: 'my_password' }
110 | ], (err, users) ->
111 | Users.list { identifiers: true }, (err, ids) ->
112 | should.not.exist err
113 | ids.length.should.eql 2
114 | for id in ids then id.should.be.a.Number()
115 | Users.clear next
116 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require should
2 | --timeout 20000
3 | --compilers coffee:coffee-script/register
4 | --reporter spec
5 | --recursive
--------------------------------------------------------------------------------
/test/remove.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get 'users'
12 | Users.identifier 'user_id'
13 | Users.unique 'username'
14 | Users.index 'email'
15 | Users = client.get 'users'
16 | next()
17 |
18 | beforeEach (next) ->
19 | Users.clear next
20 |
21 | afterEach (next) ->
22 | client.redis.keys '*', (err, keys) ->
23 | should.not.exists err
24 | keys.should.eql []
25 | next()
26 |
27 | after (next) ->
28 | client.quit next
29 |
30 | describe 'remove', ->
31 |
32 | it 'should remove a record if providing an identifier', (next) ->
33 | Users.create {
34 | username: 'my_username'
35 | email: 'my@email.com'
36 | password: 'my_password'
37 | }, (err, user) ->
38 | # Delete record based on identifier
39 | Users.remove user.user_id, (err, count) ->
40 | should.not.exist err
41 | count.should.eql 1
42 | # Check record doesn't exist
43 | Users.exists user.user_id, (err, exists) ->
44 | should.not.exist exists
45 | Users.clear next
46 |
47 | it 'should not remove a missing record', (next) ->
48 | # Delete record based on identifier
49 | Users.remove -1, (err, count) ->
50 | should.not.exist err
51 | # Count shouldn't be incremented
52 | count.should.eql 0
53 | Users.clear next
54 |
--------------------------------------------------------------------------------
/test/schema_hash.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | describe 'client', ->
8 |
9 | client = null
10 |
11 | before (next) ->
12 | client = ron config
13 | next()
14 |
15 | after (next) ->
16 | client.quit next
17 |
18 | it 'init', (next) ->
19 | next()
20 |
21 | it 'should hash a string', (next) ->
22 | client.get('users').hash('1').should.eql '356a192b7913b04c54574d18c28d46e6395428ab'
23 | next()
24 |
25 | it 'should hash a number', (next) ->
26 | client.get('users').hash(1).should.eql '356a192b7913b04c54574d18c28d46e6395428ab'
27 | next()
28 |
--------------------------------------------------------------------------------
/test/schema_temporal.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | next()
12 |
13 | afterEach (next) ->
14 | client.redis.keys '*', (err, keys) ->
15 | should.not.exists err
16 | keys.should.eql []
17 | next()
18 |
19 | after (next) ->
20 | client.quit next
21 |
22 | describe 'type', ->
23 |
24 | it 'should deal with create', (next) ->
25 | Records = client.get
26 | name: 'records'
27 | temporal: true
28 | properties:
29 | record_id: identifier: true
30 | date = new Date Date.UTC 2008, 8, 10, 16, 5, 10
31 | Records.clear (err) ->
32 | Records.create {}, (err, record) ->
33 | should.not.exist err
34 | # should v0.6.3 is broken with "instanceof Date"
35 | # https://github.com/visionmedia/should.js/issues/65
36 | (record.cdate instanceof Date).should.be.true
37 | (record.mdate instanceof Date).should.be.true
38 | # record.cdate.should.be.an.instanceof Date
39 | # record.mdate.should.be.an.instanceof Date
40 | Records.clear next
41 |
42 | it 'should deal with update', (next) ->
43 | Records = client.get
44 | name: 'records'
45 | temporal: true
46 | properties:
47 | record_id: identifier: true
48 | date = new Date Date.UTC 2008, 8, 10, 16, 5, 10
49 | Records.clear (err) ->
50 | Records.create {}, (err, record) ->
51 | cdate = record.cdate
52 | Records.update record, (err, record) ->
53 | should.not.exist err
54 | # should v0.6.3 is broken with "instanceof Date"
55 | # https://github.com/visionmedia/should.js/issues/65
56 | (record.cdate instanceof Date).should.be.true
57 | (record.mdate instanceof Date).should.be.true
58 | # record.cdate.should.be.an.instanceof Date
59 | # record.mdate.should.be.an.instanceof Date
60 | # (record.cdate is cdate).should.be.true
61 | record.cdate.getTime().should.eql cdate.getTime()
62 | Records.clear next
63 |
--------------------------------------------------------------------------------
/test/type.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | next()
12 |
13 | afterEach (next) ->
14 | client.redis.keys '*', (err, keys) ->
15 | should.not.exists err
16 | keys.should.eql []
17 | next()
18 |
19 | after (next) ->
20 | client.quit next
21 |
22 | describe 'type', ->
23 |
24 | it 'should filter properties', (next) ->
25 | Users = client.get 'users', temporal: true, properties:
26 | user_id: identifier: true
27 | username: unique: true
28 | email: index: true
29 | Users.create
30 | username: 'my_username',
31 | email: 'my@email.com',
32 | password: 'my_password'
33 | , (err, user) ->
34 | should.not.exist err
35 | properties = ['email', 'user_id']
36 | Users.unserialize user, properties: properties
37 | Object.keys(user).should.eql properties
38 | Users.clear next
39 |
--------------------------------------------------------------------------------
/test/type_date.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | next()
12 |
13 | afterEach (next) ->
14 | client.redis.keys '*', (err, keys) ->
15 | should.not.exists err
16 | keys.should.eql []
17 | next()
18 |
19 | after (next) ->
20 | client.quit next
21 |
22 | describe 'type date', ->
23 |
24 | it 'should return a record or an array depending on the provided argument', ->
25 | Records = client.get
26 | name: 'records'
27 | properties:
28 | a_date: type: 'date'
29 | date = new Date Date.UTC 2008, 8, 10, 16, 5, 10
30 | # Serialization
31 | result = Records.serialize a_date: date
32 | result.should.not.be.an.instanceof Array
33 | result.should.eql a_date: date.getTime()
34 | result = Records.serialize [a_date: date]
35 | result.should.be.an.instanceof Array
36 | result.should.eql [a_date: date.getTime()]
37 | # Deserialization
38 | result = Records.unserialize a_date: "#{date.getTime()}"
39 | result.should.not.be.an.instanceof Array
40 | result.should.eql a_date: date
41 | result = Records.unserialize [a_date: "#{date.getTime()}"]
42 | result.should.be.an.instanceof Array
43 | result.should.eql [a_date: date]
44 |
45 | it 'should parse date provided as string', ->
46 | Records = client.get
47 | name: 'records'
48 | properties:
49 | a_date: type: 'date'
50 | date = '2008-09-10'
51 | # Serialization
52 | result = Records.serialize a_date: date
53 | result.should.eql a_date: (new Date date).getTime()
54 | # Deserialization
55 | result = Records.unserialize a_date: date
56 | result.should.eql a_date: new Date date
57 |
58 | it 'should unserialize dates in seconds and milliseconds', ->
59 | Records = client.get
60 | name: 'records'
61 | properties:
62 | a_date: type: 'date'
63 | date = '2008-09-10'
64 | # Deserialization
65 | result = Records.unserialize a_date: date, { milliseconds: true }
66 | result.a_date.should.eql 1221004800000
67 | result = Records.unserialize a_date: date, { seconds: true }
68 | result.a_date.should.eql 1221004800
69 |
70 | it 'should deal with Date objects', (next) ->
71 | Records = client.get
72 | name: 'records'
73 | properties:
74 | record_id: identifier: true
75 | a_date: type: 'date'
76 | date = new Date Date.UTC 2008, 8, 10, 16, 5, 10
77 | # Test create
78 | Records.clear (err) ->
79 | Records.create
80 | a_date: date
81 | , (err, record) ->
82 | should.not.exist err
83 | recordId = record.record_id
84 | record.a_date.getTime().should.eql date.getTime()
85 | # Test all
86 | Records.all (err, records) ->
87 | should.not.exist err
88 | records.length.should.equal 1
89 | records[0].a_date.getTime().should.eql date.getTime()
90 | # Test update
91 | date.setYear 2010
92 | Records.update
93 | record_id: recordId
94 | a_date: date
95 | , (err, record) ->
96 | should.not.exist err
97 | record.a_date.getTime().should.eql date.getTime()
98 | # Test list
99 | Records.list (err, records) ->
100 | should.not.exist err
101 | records.length.should.equal 1
102 | records[0].a_date.getTime().should.eql date.getTime()
103 | # Test list
104 | Records.get records[0].record_id, (err, record) ->
105 | should.not.exist err
106 | record.a_date.getTime().should.eql date.getTime()
107 | Records.clear next
108 |
--------------------------------------------------------------------------------
/test/type_email.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get
12 | name: 'users'
13 | properties:
14 | user_id: identifier: true
15 | email: {type: 'email', index: true}
16 | next()
17 |
18 | beforeEach (next) ->
19 | Users.clear next
20 |
21 | afterEach (next) ->
22 | client.redis.keys '*', (err, keys) ->
23 | should.not.exists err
24 | keys.should.eql []
25 | next()
26 |
27 | after (next) ->
28 | client.quit next
29 |
30 | describe 'create_validation', ->
31 |
32 | it 'should validate email on creation', (next) ->
33 | Users.create
34 | email: 'invalid_email.com'
35 | , validate: true, (err, user) ->
36 | err.message.should.eql 'Invalid email invalid_email.com'
37 | user.email.should.eql 'invalid_email.com'
38 | Users.create
39 | email: 'valid@email.com'
40 | , validate: true, (err, user) ->
41 | should.not.exist err
42 | Users.clear next
43 |
44 | it 'should validate email on update', (next) ->
45 | Users.create
46 | email: 'valid@email.com'
47 | , (err, user) ->
48 | Users.update
49 | user_id: user.user_id
50 | email: 'invalid_email.com'
51 | , validate: true, (err, user) ->
52 | err.message.should.eql 'Invalid email invalid_email.com'
53 | Users.update
54 | user_id: user.user_id
55 | email: 'valid@email.com'
56 | , validate: true, (err, user) ->
57 | should.not.exist err
58 | Users.clear next
59 |
--------------------------------------------------------------------------------
/test/update.coffee:
--------------------------------------------------------------------------------
1 |
2 | should = require 'should'
3 |
4 | try config = require '../conf/test' catch e
5 | ron = require '../lib'
6 |
7 | client = Users = null
8 |
9 | before (next) ->
10 | client = ron config
11 | Users = client.get 'users'
12 | Users.identifier 'user_id'
13 | Users.unique 'username'
14 | Users.index 'email'
15 | next()
16 |
17 | beforeEach (next) ->
18 | Users.clear next
19 |
20 | afterEach (next) ->
21 | client.redis.keys '*', (err, keys) ->
22 | should.not.exists err
23 | keys.should.eql []
24 | next()
25 |
26 | after (next) ->
27 | client.quit next
28 |
29 | describe 'update', ->
30 |
31 | describe 'identifier', ->
32 |
33 | it 'missing id', (next) ->
34 | Users.update [{email: 'missing@email.com'}], (err, users) ->
35 | # Todo, could be "Record without identifier or unique properties
36 | err.message.should.eql 'Invalid record, got {"email":"missing@email.com"}'
37 | Users.clear next
38 |
39 | it 'should use unique index and fail because the provided value is not indexed', (next) ->
40 | Users.update [{username: 'missing'}], (err, users) ->
41 | err.message.should.eql 'Unsaved record'
42 | Users.clear next
43 |
44 | describe 'unique', ->
45 |
46 | it 'should update a unique value', (next) ->
47 | Users.create
48 | username: 'my_username'
49 | email: 'my@email.com'
50 | password: 'my_password'
51 | , (err, user) ->
52 | should.not.exist err
53 | user.username = 'new_username'
54 | Users.update user, (err, user) ->
55 | should.not.exist err
56 | user.username.should.eql 'new_username'
57 | Users.count (err, count) ->
58 | count.should.eql 1
59 | Users.get {username: 'my_username'}, (err, user) ->
60 | should.not.exist user
61 | Users.get {username: 'new_username'}, (err, user) ->
62 | user.username.should.eql 'new_username'
63 | Users.clear next
64 |
65 | it 'should fail to update a unique value that is already defined', (next) ->
66 | Users.create [
67 | username: 'my_username_1'
68 | email: 'my@email.com'
69 | password: 'my_password'
70 | ,
71 | username: 'my_username_2'
72 | email: 'my@email.com'
73 | password: 'my_password'
74 | ], (err, users) ->
75 | should.not.exist err
76 | user = users[0]
77 | user.username = 'my_username_2'
78 | Users.update user, (err, user) ->
79 | err.message.should.eql 'Unique value already exists'
80 | Users.clear next
81 |
82 | describe 'index', ->
83 |
84 | it 'should update an indexed property', (next) ->
85 | Users.create {
86 | username: 'my_username'
87 | email: 'my@email.com'
88 | password: 'my_password'
89 | }, (err, user) ->
90 | should.not.exist err
91 | user.email = 'new@email.com'
92 | Users.update user, (err, user) ->
93 | should.not.exist err
94 | user.email.should.eql 'new@email.com'
95 | Users.count (err, count) ->
96 | count.should.eql 1
97 | Users.list {email: 'my@email.com'}, (err, users) ->
98 | users.length.should.eql 0
99 | Users.list {email: 'new@email.com'}, (err, users) ->
100 | users.length.should.eql 1
101 | users[0].email.should.eql 'new@email.com'
102 | Users.clear next
103 |
104 | it 'should update an indexed property to null and be able to list the record', (next) ->
105 | Users.create {
106 | username: 'my_username'
107 | email: 'my@email.com'
108 | password: 'my_password'
109 | }, (err, user) ->
110 | should.not.exist err
111 | user.email = null
112 | Users.update user, (err, user) ->
113 | should.not.exist err
114 | should.not.exist user.email
115 | Users.count (err, count) ->
116 | count.should.eql 1
117 | Users.list {email: 'my@email.com'}, (err, users) ->
118 | users.length.should.eql 0
119 | Users.list {email: null}, (err, users) ->
120 | users.length.should.eql 1
121 | should.not.exist users[0].email
122 | Users.clear next
123 |
--------------------------------------------------------------------------------