├── .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[![Build Status](https://secure.travis-ci.org/adaltas/node-ron.png)](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 | --------------------------------------------------------------------------------