├── .gitignore ├── test ├── mocha.opts ├── regressions.js ├── queries.js └── basic.js ├── .travis.yml ├── test_harness.js ├── package.json ├── README.md └── lib └── ottoman.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --check-leaks -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | 6 | # 0.6 and 0.11 are not enabled as couchnode does not support 7 | # these particular versions of Node.js. -------------------------------------------------------------------------------- /test/regressions.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Ottoman = require('../lib/ottoman'); 3 | var H = require('../test_harness'); 4 | var V = Ottoman.Validator; 5 | 6 | describe('#regressions', function(){ 7 | 8 | }); -------------------------------------------------------------------------------- /test_harness.js: -------------------------------------------------------------------------------- 1 | var couchbase = require('couchbase').Mock; 2 | var cluster = new couchbase.Cluster(); 3 | 4 | var uniqueIdCounter = 0; 5 | function uniqueId(prefix) { 6 | return prefix + (uniqueIdCounter++); 7 | } 8 | 9 | module.exports.uniqueId = uniqueId; 10 | module.exports.bucket = cluster.openBucket('default'); 11 | module.exports.cbErrors = couchbase.errors; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "ODM for Couchbase Node.js driver.", 3 | "keywords": [ 4 | "couchbase", 5 | "odm", 6 | "nosql", 7 | "json", 8 | "document" 9 | ], 10 | "main": "./lib/ottoman", 11 | "license": "Apache2", 12 | "name": "ottoman", 13 | "scripts": { 14 | "test": "./node_modules/mocha/bin/mocha" 15 | }, 16 | "dependencies": { 17 | "couchbase": "~2.0.5", 18 | "underscore": "~1.5.2", 19 | "node-uuid": "~1.4.1" 20 | }, 21 | "devDependencies": { 22 | "mocha": "~1.13.0", 23 | "chai": "~1.8.1" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "http://github.com/couchbaselabs/ottoman.git" 28 | }, 29 | "version": "0.0.3" 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js ODM for Couchbase 2 | 3 | Ottoman is a ODM built for Couchbase and Node.js. It is designed to remove all 4 | of the boilerplate neccessary to build Node.js apps with Couchbase. 5 | 6 | [![Build Status](https://api.travis-ci.org/couchbaselabs/node-ottoman.png)](https://travis-ci.org/couchbaselabs/node-ottoman) 7 | 8 | 9 | ## Useful Links 10 | 11 | Source - http://github.com/couchbaselabs/node-ottoman 12 | 13 | Couchbase Node.js Community - http://couchbase.com/communities/nodejs 14 | 15 | 16 | ## Installing 17 | 18 | Ottoman is not yet published to npm, to install the in development version 19 | directly from GitHub, run: 20 | ``` 21 | npm install git+https://github.com/couchbaselabs/node-ottoman.git 22 | ``` 23 | 24 | 25 | ## Introduction 26 | 27 | Building and interacting with models using ottoman is very simple: 28 | 29 | ```javascript 30 | var User = Ottoman.model('User', { 31 | 'username': 'string', 32 | 'name': 'string', 33 | 'email': 'string' 34 | }, { 35 | bucket: new couchbase.Connection({}) 36 | }); 37 | 38 | var test = new User(); 39 | test.username = 'brett19'; 40 | test.name = 'Brett Lawson'; 41 | test.email = 'brett19@gmail.com'; 42 | 43 | Ottoman.save(test, function(err) { 44 | if (err) throw err; 45 | 46 | MyModel.findById(test._id, function(err, obj) { 47 | if (err) throw err; 48 | 49 | console.log(obj.name); 50 | // Brett Lawson 51 | }); 52 | }); 53 | 54 | ``` 55 | 56 | ## Documentation 57 | 58 | As ottoman is currently in rapid active development, documentation for it is 59 | not yet available. A good source of information in the interim should be the 60 | test cases which are available in the /test/ directory. 61 | 62 | 63 | ## Source Control 64 | 65 | The source code is available at 66 | [https://github.com/couchbaselabs/node-ottoman](https://github.com/couchbaselabs/node-ottoman). 67 | Once you have cloned the repository, you may contribute changes by submitting 68 | a GitHub Pull Request. 69 | 70 | To execute our test suite, run `npm test` from your checked out root directory. 71 | 72 | ## License 73 | 74 | Copyright 2013 Couchbase Inc. 75 | 76 | Licensed under the Apache License, Version 2.0. 77 | 78 | See [the Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0). 79 | -------------------------------------------------------------------------------- /test/queries.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Ottoman = require('../lib/ottoman'); 3 | var H = require('../test_harness'); 4 | var V = Ottoman.Validator; 5 | 6 | describe('#querying', function(){ 7 | it('should support simple queries', function(done) { 8 | var userModelName = H.uniqueId('model'); 9 | var postModelName = H.uniqueId('model'); 10 | 11 | var User = Ottoman.model(userModelName, { 12 | name: 'string' 13 | }, { 14 | constructor: function(name) { 15 | this.name = name; 16 | }, 17 | queries: { 18 | myPosts: { 19 | target: postModelName, 20 | mappedBy: 'creator', 21 | sort: 'desc', 22 | limit: 5 23 | } 24 | }, 25 | bucket: H.bucket 26 | }); 27 | 28 | var Post = Ottoman.model(postModelName, { 29 | title: 'string', 30 | creator: userModelName 31 | }, { 32 | bucket: H.bucket 33 | }); 34 | 35 | var test = new User(); 36 | test.name = 'brett19'; 37 | 38 | var post1 = new Post(); 39 | post1.title = 'Post 1'; 40 | post1.creator = test; 41 | var post2 = new Post(); 42 | post2.title = 'Post 2'; 43 | post2.creator = test; 44 | 45 | Ottoman.registerDesignDocs(function(err) { 46 | expect(err).to.be.null; 47 | 48 | Ottoman.save([test, post1, post2], function(err) { 49 | expect(err).to.be.null; 50 | 51 | test.myPosts(function(err, posts) { 52 | expect(err).to.be.null; 53 | 54 | Ottoman.load(posts, function(err) { 55 | expect(err).to.be.null; 56 | 57 | expect(posts).to.have.length(2); 58 | for (var i = 0; i < posts.length; ++i) { 59 | expect(posts[i].creator).to.equal(test); 60 | } 61 | 62 | done(); 63 | }); 64 | }); 65 | }) 66 | }); 67 | }); 68 | 69 | it.skip('should support referential documents', function(done) { 70 | var userModelName = H.uniqueId('model'); 71 | var nameKeyPrefix = H.uniqueId('kp'); 72 | 73 | var User = Ottoman.model(userModelName, { 74 | name: 'string' 75 | }, { 76 | constructor: function(name) { 77 | this.name = name; 78 | }, 79 | indexes: { 80 | getByName: { 81 | type: 'refdoc', 82 | key: ['name'] 83 | } 84 | }, 85 | bucket: H.bucket 86 | }); 87 | 88 | var test = new User(); 89 | test.name = 'brett19'; 90 | 91 | Ottoman.save(test, function(err) { 92 | expect(err).to.be.null; 93 | 94 | User.getByName('brett19', function(err, doc) { 95 | expect(doc.fname).to.equal('brett'); 96 | done(); 97 | }); 98 | }); 99 | }); 100 | 101 | it.skip('should support referential documents with custom key prefixes', function(done) { 102 | var userModelName = H.uniqueId('model'); 103 | var nameKeyPrefix = H.uniqueId('kp'); 104 | 105 | var User = Ottoman.model(userModelName, { 106 | name: 'string' 107 | }, { 108 | constructor: function(name) { 109 | this.name = name; 110 | }, 111 | indexes: { 112 | getByName: { 113 | type: 'refdoc', 114 | key: ['name'], 115 | keyPrefix: nameKeyPrefix 116 | } 117 | }, 118 | bucket: H.bucket 119 | }); 120 | 121 | var test = new User(); 122 | test.name = 'brett19'; 123 | 124 | Ottoman.save(test, function(err) { 125 | expect(err).to.be.null; 126 | 127 | User.getByName('brett19', function(err, doc) { 128 | expect(err).to.be.null; 129 | expect(test._id).to.equal(doc._id); 130 | expect(doc.fname).to.equal('brett'); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | 136 | it('should support referential documents with multiple keys', function(done) { 137 | var userModelName = H.uniqueId('model'); 138 | var nameKeyPrefix = H.uniqueId('kp'); 139 | 140 | var User = Ottoman.model(userModelName, { 141 | fname: 'string', 142 | lname: 'string' 143 | }, { 144 | constructor: function(fname, lname) { 145 | this.fname = fname; 146 | this.lname = lname; 147 | }, 148 | indexes: { 149 | getByName: { 150 | type: 'refdoc', 151 | key: ['fname', 'lname'], 152 | keyPrefix: nameKeyPrefix 153 | } 154 | }, 155 | bucket: H.bucket 156 | }); 157 | 158 | var test = new User(); 159 | test.fname = 'brett'; 160 | test.lname = 'lawson'; 161 | 162 | Ottoman.save(test, function(err) { 163 | expect(err).to.be.null; 164 | 165 | User.getByName('brett', 'lawson', function(err, doc) { 166 | expect(err).to.be.null; 167 | expect(test._id).to.equal(doc._id); 168 | expect(doc.fname, 'brett'); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | 174 | it.skip('should support handle referential document conflicts', function(done) { 175 | var userModelName = H.uniqueId('model'); 176 | var nameKeyPrefix = H.uniqueId('kp'); 177 | 178 | var User = Ottoman.model(userModelName, { 179 | name: 'string' 180 | }, { 181 | constructor: function(name) { 182 | this.name = name; 183 | }, 184 | indexes: { 185 | getByName: { 186 | type: 'refdoc', 187 | key: ['name'], 188 | keyPrefix: nameKeyPrefix 189 | } 190 | }, 191 | bucket: H.bucket 192 | }); 193 | 194 | var test = new User(); 195 | test.name = 'brett19'; 196 | 197 | Ottoman.save(test, function(err) { 198 | expect(err).to.be.null; 199 | 200 | test.name = 'frank'; 201 | 202 | Ottoman.save(test, function(err) { 203 | expect(err).to.be.null; 204 | 205 | var test2 = new User(); 206 | test2.name = 'brett19'; 207 | 208 | Ottoman.save(test2, function(err) { 209 | expect(err).to.be.null; 210 | 211 | test2.name = 'frank'; 212 | 213 | Ottoman.save(test2, function(err) { 214 | expect(err).to.exist; 215 | 216 | done(); 217 | }); 218 | }); 219 | }); 220 | }); 221 | }); 222 | 223 | 224 | }); 225 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var Ottoman = require('../lib/ottoman'); 3 | var H = require('../test_harness'); 4 | var V = Ottoman.Validator; 5 | 6 | describe('#basic', function(){ 7 | it('should handle storage/retrieval of basic types', function(done) { 8 | var modelName = H.uniqueId('model'); 9 | 10 | var MyModel = Ottoman.model(modelName, { 11 | 'str': 'string', 12 | 'boolt': 'boolean', 13 | 'boolf': 'boolean', 14 | 'int': 'integer', 15 | 'num': 'number', 16 | 'map': 'Map', 17 | 'lst': 'List' 18 | }, { 19 | bucket: H.bucket 20 | }); 21 | 22 | var test = new MyModel(); 23 | test.str = 'Brett'; 24 | test.boolt = true; 25 | test.boolf = false; 26 | test.int = 14; 27 | test.num = 99.98273; 28 | test.lst = [12,44,99]; 29 | test.map = {bob:'frank', john:'doe'}; 30 | 31 | Ottoman.save(test, function(err) { 32 | expect(err).to.be.null; 33 | 34 | MyModel.findById(test._id, function(err, obj) { 35 | expect(err).to.be.null; 36 | expect(obj.str).to.be.a('string'); 37 | expect(obj.boolt).to.be.a('boolean'); 38 | expect(obj.boolf).to.be.a('boolean'); 39 | expect(obj.int).to.be.a('number'); 40 | expect(obj.num).to.be.a('number'); 41 | expect(obj.lst).to.be.a('Array'); 42 | expect(obj.map).to.be.a('Object'); 43 | expect(obj).to.eql(test); 44 | 45 | done(); 46 | }); 47 | }); 48 | 49 | }); 50 | 51 | it('should fail to register the same model name twice', function(done) { 52 | var modelName = H.uniqueId('model'); 53 | 54 | function registerMyModel() { 55 | Ottoman.model(modelName, { 56 | 'name': 'string' 57 | }, { 58 | bucket: H.bucket 59 | }); 60 | } 61 | 62 | expect(registerMyModel).not.to.throw(); 63 | expect(registerMyModel).to.throw(Error, /already registered/); 64 | 65 | done(); 66 | }); 67 | 68 | it('should allow loading of referenced objects', function(done) { 69 | var modelName = H.uniqueId('model'); 70 | var refModelName = H.uniqueId('model'); 71 | 72 | var MyRefModel = Ottoman.model(refModelName, { 73 | 'name': 'string' 74 | }, { 75 | bucket: H.bucket 76 | }); 77 | var MyModel = Ottoman.model(modelName, { 78 | 'refObj': refModelName 79 | }, { 80 | bucket: H.bucket 81 | }); 82 | 83 | var refTest = new MyRefModel(); 84 | refTest.name = 'Fudgebars'; 85 | 86 | var myTest = new MyModel(); 87 | myTest.refObj = refTest; 88 | 89 | Ottoman.save(myTest, function(err) { 90 | expect(err).to.be.null; 91 | 92 | MyModel.findById(myTest._id, function(err, obj) { 93 | expect(err).to.be.null; 94 | 95 | Ottoman.load(obj.refObj, function(err) { 96 | expect(err).to.be.null; 97 | expect(obj.refObj.name).to.equal('Fudgebars'); 98 | 99 | done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | 105 | it('should reuse objects during deserialization', function(done) { 106 | var modelName = H.uniqueId('model'); 107 | var refModelName = H.uniqueId('model'); 108 | 109 | var MyRefModel = Ottoman.model(refModelName, { 110 | 'name': 'string' 111 | }, { 112 | bucket: H.bucket 113 | }); 114 | var MyModel = Ottoman.model(modelName, { 115 | 'refOne': refModelName, 116 | 'refTwo': refModelName 117 | }, { 118 | bucket: H.bucket 119 | }); 120 | 121 | var refTest = new MyRefModel(); 122 | refTest.name = 'Franklin'; 123 | 124 | var myTest = new MyModel(); 125 | myTest.refOne = refTest; 126 | myTest.refTwo = refTest; 127 | 128 | Ottoman.save(myTest, function(err) { 129 | expect(err).to.be.null; 130 | 131 | MyModel.findById(myTest._id, function(err, obj) { 132 | expect(err).to.be.null; 133 | expect(obj.refOne).to.equal(obj.refTwo); 134 | 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | it('should callback even with no changes to save', function(done) { 141 | var modelName = H.uniqueId('model'); 142 | 143 | var MyModel = Ottoman.model(modelName, { 144 | 'name': String 145 | }, { 146 | bucket: H.bucket 147 | }); 148 | 149 | var test = new MyModel(); 150 | test.name = 'Matt'; 151 | 152 | Ottoman.save(test, function(err) { 153 | expect(err).to.be.null; 154 | 155 | Ottoman.save(test, function(err) { 156 | expect(err).to.be.null; 157 | 158 | done(); 159 | }); 160 | }) 161 | }); 162 | 163 | it('should be able to handle direct type references', function(done) { 164 | var modelName = H.uniqueId('model'); 165 | 166 | var MyModel = Ottoman.model(modelName, { 167 | 'name': String 168 | }, { 169 | bucket: H.bucket 170 | }); 171 | 172 | var test = new MyModel(); 173 | test.name = 'Matt'; 174 | 175 | Ottoman.save(test, function(err) { 176 | expect(err).to.be.null; 177 | 178 | MyModel.findById(test._id, function(err, obj) { 179 | expect(err).to.be.null; 180 | expect(obj.name).to.be.a('string'); 181 | expect(obj.name).to.equal('Matt'); 182 | 183 | done(); 184 | }); 185 | }) 186 | }); 187 | 188 | it('should be able to handle self references', function(done) { 189 | var modelName = H.uniqueId('model'); 190 | 191 | var MyModel = Ottoman.model(modelName, { 192 | 'name': 'string', 193 | 'self': modelName 194 | }, { 195 | bucket: H.bucket 196 | }); 197 | 198 | var test = new MyModel(); 199 | test.name = 'Matt'; 200 | test.self = test; 201 | 202 | Ottoman.save(test, function(err) { 203 | expect(err).to.be.null; 204 | 205 | MyModel.findById(test._id, function(err, obj) { 206 | expect(err).to.be.null; 207 | expect(obj.self).to.equal(obj); 208 | 209 | done(); 210 | }); 211 | }) 212 | }); 213 | 214 | it('should be able to handle circular references', function(done) { 215 | var modelName = H.uniqueId('model'); 216 | var refModelName = H.uniqueId('model'); 217 | 218 | var MyRefModel = Ottoman.model(refModelName, { 219 | 'name': 'string', 220 | 'parent': modelName 221 | }, { 222 | bucket: H.bucket 223 | }); 224 | var MyModel = Ottoman.model(modelName, { 225 | 'child': refModelName 226 | }, { 227 | bucket: H.bucket 228 | }); 229 | 230 | var refTest = new MyRefModel(); 231 | var myTest = new MyModel(); 232 | myTest.child = refTest; 233 | refTest.name = 'Darling'; 234 | refTest.parent = myTest; 235 | 236 | Ottoman.save(myTest, function(err) { 237 | expect(err).to.be.null; 238 | 239 | MyModel.findById(myTest._id, function(err, obj) { 240 | expect(err).to.be.null; 241 | 242 | Ottoman.load(obj.child, function(err) { 243 | expect(err).to.be.null; 244 | expect(obj.child.parent).to.equal(obj); 245 | 246 | done(); 247 | }); 248 | }); 249 | }); 250 | }); 251 | 252 | it('should fail to deserialize another type', function(done) { 253 | var modelNameOne = H.uniqueId('model'); 254 | var modelNameTwo = H.uniqueId('model'); 255 | 256 | var MyModelOne = Ottoman.model(modelNameOne, { 257 | 'name': 'string' 258 | }, { 259 | bucket: H.bucket 260 | }); 261 | var MyModelTwo = Ottoman.model(modelNameTwo, { 262 | 'name': 'string' 263 | }, { 264 | bucket: H.bucket 265 | }); 266 | 267 | var test = new MyModelOne(); 268 | test.name = 'Joseph'; 269 | 270 | Ottoman.save(test, function(err) { 271 | expect(err).to.be.null; 272 | 273 | MyModelTwo.findById(test._id, function(err, obj) { 274 | expect(err).to.exist; 275 | expect(err.code).to.equal(H.cbErrors.keyNotFound); 276 | 277 | done(); 278 | }); 279 | }); 280 | }); 281 | 282 | it('should call constructor', function(done) { 283 | var modelName = H.uniqueId('model'); 284 | 285 | var MyModel = Ottoman.model(modelName, { 286 | 'name': { type: 'string' } 287 | }, { 288 | constructor: function(name) { 289 | this.name = name; 290 | }, 291 | bucket: H.bucket 292 | }); 293 | 294 | var test = new MyModel('Josephina'); 295 | expect(test.name).to.equal('Josephina'); 296 | 297 | done(); 298 | }); 299 | 300 | it('should enforce readonly attribute', function(done) { 301 | var modelName = H.uniqueId('model'); 302 | 303 | var MyModel = Ottoman.model(modelName, { 304 | 'name': { type: 'string', readonly: true } 305 | }, { 306 | constructor: function(name) { 307 | this.name = name; 308 | }, 309 | bucket: H.bucket 310 | }); 311 | 312 | var test = new MyModel('Franklin'); 313 | 314 | function testNameChange() { 315 | test.name = 'Bobby'; 316 | } 317 | expect(testNameChange).to.throw(Error, /read\-only/); 318 | expect(test.name).to.equal('Franklin'); 319 | 320 | done(); 321 | }); 322 | 323 | it('should support alternate type descriminators', function(done) { 324 | var modelName = H.uniqueId('model'); 325 | 326 | var MyModel = Ottoman.model(modelName, { 327 | 'name': 'string' 328 | }, { 329 | descriminators: { 330 | descrimTestField: modelName + 'Descrim' 331 | }, 332 | bucket: H.bucket 333 | }); 334 | 335 | var test = new MyModel(); 336 | test.name = 'Mike'; 337 | 338 | Ottoman.save(test, function(err) { 339 | expect(err).to.be.null; 340 | 341 | H.bucket.get(Ottoman.key(test), {}, function(err, result) { 342 | expect(err).to.be.null; 343 | expect(result.value._type).to.be.undefined; 344 | expect(result.value.descrimTestField).to.equal(modelName + 'Descrim'); 345 | 346 | done(); 347 | }); 348 | }); 349 | }); 350 | 351 | it('should deserialized mixed lists', function(done) { 352 | var modelNameOne = H.uniqueId('model'); 353 | var modelNameTwo = H.uniqueId('model'); 354 | var modelNameThree = H.uniqueId('model'); 355 | 356 | var MyModelOne = Ottoman.model(modelNameOne, { 357 | 'mixedlst': { type: 'List', subtype:'Mixed' } 358 | }, { 359 | bucket: H.bucket 360 | }); 361 | var MyModelTwo = Ottoman.model(modelNameTwo, { 362 | 'name': 'string' 363 | }, { 364 | bucket: H.bucket 365 | }); 366 | var MyModelThree = Ottoman.model(modelNameThree, { 367 | 'email': 'string' 368 | }, { 369 | bucket: H.bucket 370 | }); 371 | 372 | var testRefTwo = new MyModelTwo(); 373 | testRefTwo.name = 'Joe'; 374 | var testRefThree = new MyModelThree(); 375 | testRefThree.email = 'joe@test.com'; 376 | 377 | var test = new MyModelOne(); 378 | test.mixedlst = [testRefTwo, testRefThree]; 379 | 380 | Ottoman.save(test, function(err) { 381 | expect(err).to.be.null; 382 | 383 | MyModelOne.findById(test._id, function(err, obj) { 384 | expect(err).to.be.null; 385 | expect(obj.mixedlst[0]).to.be.an.instanceof(MyModelTwo); 386 | expect(obj.mixedlst[1]).to.be.an.instanceof(MyModelThree); 387 | 388 | done(); 389 | }); 390 | }) 391 | }); 392 | 393 | it('should support field aliasing', function(done) { 394 | var modelName = H.uniqueId('model'); 395 | 396 | var MyModel = Ottoman.model(modelName, { 397 | 'name': { type: 'string', name: 'n' }, 398 | 'email': { type: 'string', name: 'e' } 399 | }, { 400 | bucket: H.bucket 401 | }); 402 | 403 | var test = new MyModel(); 404 | test.name = 'Michael'; 405 | test.email = 'mick@test.com'; 406 | 407 | Ottoman.save(test, function(err) { 408 | expect(err).to.be.null; 409 | 410 | MyModel.findById(test._id, function(err, obj) { 411 | expect(err).to.be.null; 412 | expect(obj).to.eql(test); 413 | 414 | H.bucket.get(Ottoman.key(test), {}, function(err, result) { 415 | expect(err).to.be.null; 416 | expect(result.value.n).to.equal(test.name); 417 | expect(result.value.e).to.equal(test.email); 418 | 419 | done(); 420 | }); 421 | }); 422 | }); 423 | }); 424 | 425 | it('should not allow fields to share an alias', function(done){ 426 | var modelName = H.uniqueId('model'); 427 | 428 | function registerModel() { 429 | var MyModel = Ottoman.model(modelName, { 430 | 'name': { type: 'string', name: 'n' }, 431 | 'number': { type: 'string', name: 'n' } 432 | }, { 433 | bucket: H.bucket 434 | }); 435 | }; 436 | expect(registerModel).to.throw(Error, /multiple/); 437 | 438 | done(); 439 | }); 440 | 441 | it('should allow custom id fields', function(done){ 442 | var modelName = H.uniqueId('model'); 443 | 444 | var MyModel = Ottoman.model(modelName, { 445 | 'myId': { type: 'string', readonly: true }, 446 | 'name': { type: 'string', name: 'n' } 447 | }, { 448 | constructor: function(myId) { 449 | this.myId = myId; 450 | }, 451 | id: ['myId'], 452 | bucket: H.bucket 453 | }); 454 | 455 | var randomId = H.uniqueId('id'); 456 | var test = new MyModel(randomId); 457 | test.name = 'Bobby'; 458 | 459 | Ottoman.save(test, function(err) { 460 | expect(err).to.be.null; 461 | 462 | H.bucket.get(Ottoman.key(test), {}, function(err, result) { 463 | expect(err).to.be.null; 464 | expect(result.value._id).to.be.undefined; 465 | expect(result.value.myId).to.equal(randomId); 466 | 467 | done(); 468 | }); 469 | }); 470 | }); 471 | 472 | it('should enforce custom ids being readonly', function(done) { 473 | var modelName = H.uniqueId('model'); 474 | 475 | function registerModel() { 476 | var MyModel = Ottoman.model(modelName, { 477 | 'myId': { type: 'string' }, 478 | 'name': { type: 'string' } 479 | }, { 480 | constructor: function(myId) { 481 | this.myId = myId; 482 | }, 483 | id: ['myId'], 484 | bucket: H.bucket 485 | }); 486 | } 487 | expect(registerModel).to.throw(Error, /readonly/); 488 | 489 | done(); 490 | }); 491 | 492 | it('should have an accurate name', function(done) { 493 | var modelName = H.uniqueId('model'); 494 | 495 | var MyModel = Ottoman.model(modelName, { 496 | 'name': 'string' 497 | }, { 498 | bucket: H.bucket 499 | }); 500 | 501 | expect(MyModel.name).to.equal(modelName); 502 | 503 | done(); 504 | }); 505 | 506 | it('should support Date types', function(done) { 507 | var modelName = H.uniqueId('model'); 508 | 509 | var MyModel = Ottoman.model(modelName, { 510 | 'name': 'string', 511 | 'lastLogin': 'Date', 512 | 'mixedDate': 'Mixed' 513 | }, { 514 | bucket: H.bucket 515 | }); 516 | 517 | var test = new MyModel(); 518 | test.name = 'Bratok'; 519 | test.lastLogin = new Date(); 520 | test.mixedDate = new Date(); 521 | 522 | Ottoman.save(test, function(err) { 523 | expect(err).to.be.null; 524 | 525 | MyModel.findById(test._id, function(err, obj) { 526 | expect(err).to.be.null; 527 | expect(obj.lastLogin).to.be.an.instanceof(Date); 528 | expect(obj.mixedDate).to.be.an.instanceof(Date); 529 | expect(obj).to.eql(test); 530 | 531 | done(); 532 | }); 533 | }); 534 | }); 535 | 536 | it('should support validators', function(done) { 537 | var modelName = H.uniqueId('model'); 538 | 539 | var MyModel = Ottoman.model(modelName, { 540 | 'val': { type: 'integer', validator: V.min(100) } 541 | }, { 542 | bucket: H.bucket 543 | }); 544 | 545 | var test = new MyModel(); 546 | 547 | test.val = 99; 548 | Ottoman.save(test, function(err) { 549 | expect(err).to.exist; 550 | 551 | test.val = 100; 552 | Ottoman.save(test, function(err) { 553 | expect(err).to.be.null; 554 | 555 | test.val = 101; 556 | Ottoman.save(test, function(err) { 557 | expect(err).to.be.null; 558 | 559 | done(); 560 | }); 561 | }); 562 | }) 563 | }); 564 | 565 | it('should allow private fields to restrict toJSON values', function(done) { 566 | var modelName = H.uniqueId('model'); 567 | 568 | var MyModel = Ottoman.model(modelName, { 569 | 'valx': { type: 'integer', private: true }, 570 | 'valy': { type: 'integer', private: false }, 571 | 'valz': { type: 'integer' } 572 | }, { 573 | bucket: H.bucket 574 | }); 575 | 576 | var test = new MyModel(); 577 | test.valx = 19; 578 | test.valy = 32; 579 | test.valz = 65; 580 | 581 | var objy = test.toJSON(); 582 | var objz = test.toJSON(true); 583 | 584 | expect(objy.valx).to.not.exist; 585 | expect(objy.valy).to.exist; 586 | expect(objy.valz).to.exist; 587 | 588 | expect(objz.valx).to.exist; 589 | expect(objz.valy).to.exist; 590 | expect(objz.valz).to.exist; 591 | 592 | expect(JSON.stringify(objy)).to.equal(JSON.stringify(test.toJSON())); 593 | 594 | done(); 595 | }); 596 | }); 597 | -------------------------------------------------------------------------------- /lib/ottoman.js: -------------------------------------------------------------------------------- 1 | /* 2 | Add querying and design doc building. 3 | Need to set up handling for dealing with non-loaded 4 | objects so users can't try and use them. 5 | Add validation support. 6 | */ 7 | 8 | var util = require('util'); 9 | var uuid = require('node-uuid'); 10 | var _ = require('underscore'); 11 | var couchbase = require('couchbase'); 12 | 13 | var CORETYPES = ['string', 'integer', 'number', 'boolean']; 14 | var INTERNALGIZMO = new Object(); 15 | 16 | var TYPEALIASES = { 17 | 'String': 'string', 18 | 'Number': 'number', 19 | 'Boolean': 'boolean' 20 | }; 21 | 22 | // A map of all Ottoman types that have been registered. 23 | var typeList = {}; 24 | var queries = []; 25 | 26 | /** 27 | * Returns a ottoman type name by matching the input 28 | * object against the type descriminators. 29 | * @param {Object} obx 30 | * @returns {string} 31 | */ 32 | function typeNameFromObx(obx) { 33 | for (var i in typeList) { 34 | if (typeList.hasOwnProperty(i)) { 35 | var info = typeList[i].prototype.$; 36 | 37 | var matches = true; 38 | for (var j in info.descrims) { 39 | if (info.descrims.hasOwnProperty(j)) { 40 | if (obx[j] != info.descrims[j]) { 41 | matches = false; 42 | break; 43 | } 44 | } 45 | } 46 | if (matches) { 47 | return info.name; 48 | } 49 | } 50 | } 51 | 52 | return null; 53 | } 54 | 55 | /** 56 | * Determins if an obx object is a ottoman type. 57 | * @param {Object} obx 58 | * @returns {boolean} 59 | */ 60 | function isOttoObx(obx) { 61 | if (obx instanceof Object && typeNameFromObx(obx)) { 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | var ISO8601REGEX = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$/; 68 | /** 69 | * Determines if an obx object is a date. 70 | * @param {Object} obx 71 | * @returns {boolean} 72 | */ 73 | function isDateObx(obx) { 74 | if (typeof(obx) === 'string' && obx.match(ISO8601REGEX)) { 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | /** 81 | * Determines if an obx object is a reference to another document. 82 | * @param {Object} obx 83 | * @returns {boolean} 84 | */ 85 | function isRefObx(obx) { 86 | if (obx instanceof Object && obx['$ref']) { 87 | return true; 88 | } 89 | } 90 | 91 | /** 92 | * Scans through the ottoman type list to identify if this object 93 | * is an instance of a ottoman type. 94 | * @param {Object} obj 95 | * @returns {boolean} 96 | */ 97 | function isOttoObj(obj) { 98 | for (var i in typeList) { 99 | if (typeList.hasOwnProperty(i)) { 100 | if (obj instanceof typeList[i]) { 101 | return true; 102 | } 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | 109 | 110 | function obxToObj_Otto_Load(obj, obx, depth, objCache) { 111 | var info = obj.$; 112 | 113 | if (!(obx instanceof Object)) { 114 | throw new Error('expected value of type Object'); 115 | } 116 | 117 | // Lets check for sanity sake 118 | var obxTypeName = typeNameFromObx(obx); 119 | if (obxTypeName !== obj.$.name) { 120 | throw new Error('data is wrong type'); 121 | } 122 | 123 | obj.$values = {}; 124 | obj.$loaded = true; 125 | obj.$initial = obx; 126 | 127 | for (var i in info.schema) { 128 | if (info.schema.hasOwnProperty(i)) { 129 | var field = info.schema[i]; 130 | 131 | var subtypes = []; 132 | if (field.subtype) { 133 | subtypes.push(field.subtype); 134 | } 135 | 136 | var newObj = obxToObj(obx[field.name], field.type, subtypes, depth+1, objCache, null); 137 | if (newObj !== undefined) { 138 | obj.$values[field.name] = newObj; 139 | } 140 | } 141 | } 142 | } 143 | 144 | function obxToObj_Otto(obx, typeName, subtypeNames, depth, objCache, thisKey) { 145 | var type = typeList[typeName]; 146 | 147 | if (isRefObx(obx)) { 148 | var refkey = obx['$ref'][1]; 149 | 150 | // Referenced 151 | if (!(obx instanceof Object)) { 152 | throw new Error('expected object to be an Object') 153 | } 154 | if (obx['$ref'].length !== 2) { 155 | throw new Error('expected reference object'); 156 | } 157 | if (obx['$ref'][0] !== typeName) { 158 | throw new Error('data is wrong type'); 159 | } 160 | 161 | // Check the cache 162 | var cachedObj = objCache[refkey]; 163 | if (cachedObj) { 164 | if (cachedObj.$.name !== obx['$ref'][0]) { 165 | throw new Error('object cached but later found as different type'); 166 | } 167 | return cachedObj; 168 | } 169 | 170 | // Create Object 171 | var obj = createRefObj(refkey, typeName, objCache); 172 | return obj; 173 | } else { 174 | // Embedded 175 | if (thisKey === undefined) { 176 | throw new Error('internal: thisKey should be null or a string'); 177 | } 178 | 179 | // Create Object 180 | var obj = createRefObj(thisKey, typeName, objCache); 181 | 182 | // Populate data 183 | obxToObj_Otto_Load(obj, obx, depth+1, objCache); 184 | return obj; 185 | } 186 | } 187 | 188 | function createRefObj(key, typeName, objCache) { 189 | var type = typeList[typeName]; 190 | if (!type) { 191 | throw new Error('unknown type ' + typeName); 192 | } 193 | 194 | var obj = new type(INTERNALGIZMO); 195 | obj.$key = key; 196 | obj.$cas = null; 197 | obj.$cache = objCache; 198 | 199 | // Default as unloaded and blank, this is overwritten immediately 200 | // by obxToObj_Otto_Load a lot of the times. 201 | obj.$values = null; 202 | obj.$loaded = false; 203 | obj.$initial = null; 204 | 205 | objCache[key] = obj; 206 | return obj; 207 | } 208 | 209 | function obxToObj_List(obx, typeName, subtypeNames, depth, objCache, thisKey) { 210 | if (!Array.isArray(obx)) { 211 | throw new Error('expected array'); 212 | } 213 | 214 | if (!subtypeNames || subtypeNames.length == 0) { 215 | subtypeNames = ['Mixed']; 216 | } 217 | 218 | var out = []; 219 | for (var i = 0; i < obx.length; ++i) { 220 | var newObj = obxToObj(obx[i], subtypeNames[0], subtypeNames.slice(1), depth+1, objCache, null); 221 | if (newObj !== undefined) { 222 | out[i] = newObj; 223 | } 224 | } 225 | return out; 226 | } 227 | 228 | function obxToObj_Map(obx, typeName, subtypeNames, depth, objCache, thisKey) { 229 | if (!(obx instanceof Object)) { 230 | throw new Error('expected object'); 231 | } 232 | 233 | if (!subtypeNames || subtypeNames.length == 0) { 234 | subtypeNames = ['Mixed']; 235 | } 236 | 237 | var out = {}; 238 | for (var i in obx) { 239 | if (obx.hasOwnProperty(i)) { 240 | var newObj = obxToObj(obx[i], subtypeNames[0], subtypeNames.slice(1), depth+1, objCache, null); 241 | if (newObj !== undefined) { 242 | out[i] = newObj; 243 | } 244 | } 245 | } 246 | return out; 247 | } 248 | 249 | function obxToObj_Date(obx, typeName, subtypeNames, depth, objCache, thisKey) { 250 | if (typeof(obx) !== 'string') { 251 | throw new Error('expected string'); 252 | } 253 | 254 | return new Date(obx); 255 | } 256 | 257 | function obxToObj_Mixed(obx, typeName, subtypeNames, depth, objCache, thisKey) { 258 | if (isRefObx(obx)) { 259 | return obxToObj_Otto(obx, obx['$ref'][0], null, depth, objCache, thisKey); 260 | } else if (isOttoObx(obx)) { 261 | var realTypeName = typeNameFromObx(obx); 262 | return obxToObj_Otto(obx, realTypeName, null, depth, objCache, thisKey); 263 | } else if (isDateObx(obx)) { 264 | return obxToObj_Date(obx, 'Date', null, depth, objCache, thisKey); 265 | } else if (Array.isArray(obx)) { 266 | return obxToObj_List(obx, 'List', null, depth, objCache, thisKey); 267 | } else if (obx instanceof Object) { 268 | return obxToObj_Map(obx, 'Map', null, depth, objCache, thisKey); 269 | } else { 270 | return obx; 271 | } 272 | } 273 | 274 | function obxToObj(obx, typeName, subtypeNames, depth, objCache, thisKey) { 275 | if (!typeName) { 276 | typeName = 'Mixed'; 277 | } 278 | 279 | if (obx === undefined) { 280 | return undefined; 281 | } else if (obx === null) { 282 | return null; 283 | } 284 | 285 | if (typeList[typeName]) { 286 | return obxToObj_Otto(obx, typeName, subtypeNames, depth, objCache, thisKey); 287 | } else if (typeName === 'Date') { 288 | return obxToObj_Date(obx, typeName, subtypeNames, depth, objCache, thisKey); 289 | } else if (typeName === 'List') { 290 | return obxToObj_List(obx, typeName, subtypeNames, depth, objCache, thisKey); 291 | } else if (typeName === 'Map') { 292 | return obxToObj_Map(obx, typeName, subtypeNames, depth, objCache, thisKey); 293 | } else if (typeName === 'Mixed') { 294 | return obxToObj_Mixed(obx, typeName, subtypeNames, depth, objCache, thisKey); 295 | } else if (CORETYPES.indexOf(typeName) >= 0) { 296 | if (obx instanceof Object) { 297 | throw new Error('core type is an object'); 298 | } 299 | return obx; 300 | } else { 301 | throw new Error('encountered unknown type ' + typeName); 302 | } 303 | } 304 | 305 | 306 | 307 | 308 | 309 | 310 | function objToObx_Otto(obj, typeName, subtypeNames, depth, objRefs) { 311 | if (!(obj instanceof typeList[typeName])) { 312 | throw new Error('expected object of type ' + typeName); 313 | } 314 | 315 | if (depth > 0 && !obj.$.embed) { 316 | // Add to refs array, but only if its not already there. 317 | if (objRefs.indexOf(obj) < 0) { 318 | objRefs.push(obj); 319 | } 320 | 321 | return {'$ref': [obj.$.name, modelKey.call(obj)]}; 322 | } else { 323 | // Some shortcuts 324 | var info = obj.$; 325 | var schema = info.schema; 326 | var values = obj.$values; 327 | 328 | var out = {}; 329 | 330 | // Add schema fields 331 | for (var i in schema) { 332 | if (schema.hasOwnProperty(i)) { 333 | var field = schema[i]; 334 | var subtypes = []; 335 | if (field.subtype) { 336 | subtypes.push(field.subtype); 337 | } 338 | 339 | var outObj = objToObx(values[field.name], field.type, subtypes, depth+1, objRefs); 340 | if (outObj !== undefined) { 341 | out[field.name] = outObj; 342 | } 343 | } 344 | } 345 | 346 | // Add descriminators 347 | for (var i in info.descrims) { 348 | if (info.descrims.hasOwnProperty(i)) { 349 | out[i] = info.descrims[i]; 350 | } 351 | } 352 | 353 | return out; 354 | } 355 | } 356 | 357 | function objToObx_List(obj, typeName, subtypeNames, depth, objRefs) { 358 | if (!subtypeNames || subtypeNames.length == 0) { 359 | subtypeNames = ['Mixed']; 360 | } 361 | 362 | var out = []; 363 | for (var i = 0; i < obj.length; ++i) { 364 | var outObj = objToObx(obj[i], subtypeNames[0], subtypeNames.slice(1), depth+1, objRefs); 365 | if (outObj !== undefined) { 366 | out[i] = outObj; 367 | } 368 | } 369 | return out; 370 | } 371 | 372 | function objToObx_Map(obj, typeName, subtypeNames, depth, objRefs) { 373 | if (!subtypeNames || subtypeNames.length == 0) { 374 | subtypeNames = ['Mixed']; 375 | } 376 | 377 | var out = {}; 378 | for (var i in obj) { 379 | if (obj.hasOwnProperty(i)) { 380 | var outObj = objToObx(obj[i], subtypeNames[0], subtypeNames.slice(1), depth+1, objRefs); 381 | if (outObj !== undefined) { 382 | out[i] = outObj; 383 | } 384 | } 385 | } 386 | return out; 387 | } 388 | 389 | function objToObx_Date(obj, typeName, subtypeNames, depth, objRefs) { 390 | if (!(obj instanceof Date)) { 391 | throw new Error('expected Date object'); 392 | } 393 | return obj.toJSON(); 394 | } 395 | 396 | function objToObx_Mixed(obj, typeName, subtypeNames, depth, objRefs) { 397 | if (isOttoObj(obj)) { 398 | return objToObx_Otto(obj, obj.$.name, null, depth, objRefs); 399 | } else if (obj instanceof Date) { 400 | return objToObx_Date(obj, 'Date', null, depth, objRefs); 401 | } else if (Array.isArray(obj)) { 402 | return objToObx_List(obj, 'List', null, depth, objRefs); 403 | } else if (obj instanceof Object) { 404 | return objToObx_Map(obj, 'Map', null, depth, objRefs); 405 | } else { 406 | return obj; 407 | } 408 | } 409 | 410 | function objToObx(obj, typeName, subtypeNames, depth, objRefs) { 411 | if (!typeName) { 412 | typeName = 'Mixed'; 413 | } 414 | 415 | if (obj === undefined) { 416 | return undefined; 417 | } else if (obj === null) { 418 | return null; 419 | } 420 | 421 | if (typeList[typeName]) { 422 | return objToObx_Otto(obj, typeName, subtypeNames, depth, objRefs); 423 | } else if (typeName === 'Date') { 424 | return objToObx_Date(obj, typeName, subtypeNames, depth, objRefs); 425 | } else if (typeName === 'List') { 426 | return objToObx_List(obj, typeName, subtypeNames, depth, objRefs); 427 | } else if (typeName === 'Map') { 428 | return objToObx_Map(obj, typeName, subtypeNames, depth, objRefs); 429 | } else if (typeName === 'Mixed') { 430 | return objToObx_Mixed(obj, typeName, subtypeNames, depth, objRefs); 431 | } else if (CORETYPES.indexOf(typeName) >= 0) { 432 | if (obj instanceof Object) { 433 | throw new Error('core type is an object'); 434 | } 435 | return obj; 436 | } else { 437 | throw new Error('encountered unknown type ' + typeName); 438 | } 439 | } 440 | 441 | function serialize(obj) { 442 | return objToObx(obj, obj.$.name, null, 0, []); 443 | } 444 | module.exports.serialize = serialize; 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | function _buildRefDocName(modelName, obj, options) { 453 | if (!obj) { 454 | return null; 455 | } 456 | 457 | var name = ''; 458 | 459 | if (options.keyPrefix) { 460 | name = options.keyPrefix; 461 | } else { 462 | name = modelName; 463 | for (var i = 0; i < options.key.length; ++i) { 464 | name += '_' + options.key[i]; 465 | } 466 | } 467 | 468 | for (var i = 0; i < options.key.length; ++i) { 469 | if (Array.isArray(obj)) { 470 | name += '-' + obj[i]; 471 | } else { 472 | name += '-' + obj[options.key[i]]; 473 | } 474 | } 475 | 476 | return name; 477 | } 478 | 479 | function _saveObj(obj, key, doc, callback) { 480 | var refDocAdds = []; 481 | var refDocRemoves = []; 482 | 483 | if (obj.$.refdocs) { 484 | var refdocs = obj.$.refdocs; 485 | for (var i = 0; i < refdocs.length; ++i) { 486 | var newRef = _buildRefDocName(obj.$.name, obj, refdocs[i]); 487 | var oldRef = _buildRefDocName(obj.$.name, obj.$initial, refdocs[i]); 488 | 489 | if (oldRef !== newRef) { 490 | refDocAdds.push(newRef); 491 | if (oldRef) { 492 | refDocRemoves.push(oldRef); 493 | } 494 | } 495 | } 496 | } 497 | 498 | var curIdx = 0; 499 | var stage = 0; 500 | 501 | (function doNext() { 502 | if (stage === 0) { 503 | if (curIdx >= refDocAdds.length) { 504 | curIdx = 0; 505 | stage = 2; 506 | return doNext(); 507 | } 508 | 509 | obj.$.bucket.insert(refDocAdds[curIdx], key, function(e) { 510 | if (e) { 511 | // Begin Rollback 512 | curIdx--; 513 | stage = 1; 514 | return doNext(); 515 | } 516 | 517 | curIdx++; 518 | return doNext(); 519 | }); 520 | } else if (stage === 1) { 521 | if (curIdx < 0) { 522 | curIdx = 0; 523 | return callback('refdoc conflict'); 524 | } 525 | 526 | obj.$.bucket.remove(refDocAdds[curIdx], function() { 527 | curIdx--; 528 | return doNext(); 529 | }); 530 | } else if (stage === 2) { 531 | obj.$initial = doc; 532 | obj.$.bucket.upsert(key, doc, {cas: obj.$cas}, function(){ 533 | stage = 3; 534 | return doNext(); 535 | }); 536 | } else if (stage === 3) { 537 | if (curIdx >= refDocRemoves.length) { 538 | curIdx = 0; 539 | stage = 4; 540 | return doNext(); 541 | } 542 | 543 | obj.$.bucket.remove(refDocRemoves[curIdx], function() { 544 | curIdx++; 545 | return doNext(); 546 | }); 547 | } else if (stage === 4) { 548 | return callback(); 549 | } 550 | })(); 551 | } 552 | 553 | function save(objs, callback) { 554 | if (!Array.isArray(objs)) { 555 | objs = [objs]; 556 | } 557 | 558 | var saved = 0; 559 | var errors = []; 560 | var toSave = []; 561 | 562 | for (var i = 0; i < objs.length; ++i) { 563 | if (!objs[i].$loaded) { 564 | continue; 565 | } 566 | 567 | // Do validations 568 | try { 569 | modelDoValidation.call(objs[i]); 570 | } catch(e) { 571 | return callback(e); 572 | } 573 | 574 | var key = modelKey.call(objs[i]); 575 | var doc = objToObx(objs[i], objs[i].$.name, null, 0, objs); 576 | 577 | if (!_.isEqual(objs[i].$initial, doc)) { 578 | toSave.push([objs[i], key, doc]); 579 | } 580 | } 581 | 582 | if (toSave.length > 0) { 583 | for (var i = 0; i < toSave.length; ++i) { 584 | var obj = toSave[i][0]; 585 | var key = toSave[i][1]; 586 | var doc = toSave[i][2]; 587 | 588 | _saveObj(obj, key, doc, function(e) { 589 | if (e) { 590 | errors.push(e); 591 | } 592 | saved++; 593 | if (saved === toSave.length) { 594 | if (callback) { 595 | if (errors.length === 0) { 596 | callback(null); 597 | } else { 598 | // TODO: Properly handle errors here 599 | callback(errors); 600 | } 601 | 602 | } 603 | } 604 | }); 605 | } 606 | } else { 607 | // Nothing to save 608 | callback(null); 609 | } 610 | 611 | } 612 | module.exports.save = save; 613 | 614 | 615 | function _loadRefs(obj, depthLeft, callback) { 616 | var refs = []; 617 | objToObx(obj, obj.$.name, null, 0, refs); 618 | 619 | if (refs.length === 0) { 620 | callback(null); 621 | return; 622 | } 623 | 624 | var loaded = 0; 625 | for (var i = 0; i < refs.length; ++i) { 626 | _load(refs[i], depthLeft-1, function(err) { 627 | loaded++; 628 | if (loaded >= refs.length) { 629 | callback(null); 630 | } 631 | }) 632 | } 633 | } 634 | function _load(obj, depthLeft, callback) { 635 | if (depthLeft === 0) { 636 | callback(null); 637 | return; 638 | } 639 | 640 | if (isOttoObj(obj)) { 641 | if (obj.$loaded) { 642 | _loadRefs(obj, depthLeft, callback); 643 | } else { 644 | var key = modelKey.call(obj); 645 | obj.$.bucket.get(key, {}, function(err, result) { 646 | if (err) { 647 | return callback(err); 648 | } 649 | obxToObj_Otto_Load(obj, result.value, 0, obj.$cache, key); 650 | obj.$cas = result.cas; 651 | _loadRefs(obj, depthLeft, callback); 652 | }); 653 | } 654 | } else if (Array.isArray(obj)) { 655 | var loaded = 0; 656 | for (var i = 0; i < obj.length; ++i) { 657 | _load(obj[i], depthLeft, function(err) { 658 | loaded++; 659 | if (loaded === obj.length) { 660 | // TODO: Only returns last error 661 | callback(err); 662 | } 663 | }); 664 | } 665 | } else if (obj instanceof Object) { 666 | var needsLoad = 0; 667 | for (var i in obj) { 668 | if (obj.hasOwnProperty(i)) { 669 | needsLoad++; 670 | _load(obj[i], depthLeft, function(err) { 671 | needsLoad--; 672 | if (needsLoad === 0) { 673 | // TODO: Only returns last error 674 | callback(err); 675 | } 676 | }) 677 | } 678 | } 679 | } else { 680 | console.warn('attempted to call Load on a core type.'); 681 | } 682 | } 683 | function load(obj, options, callback) { 684 | if (arguments.length === 2) { 685 | callback = options; 686 | options = {}; 687 | } 688 | if (!options.depth || options.depth < 1) { 689 | options.depth = 1; 690 | } 691 | 692 | _load(obj, options.depth, callback); 693 | } 694 | module.exports.load = load; 695 | 696 | 697 | 698 | 699 | 700 | 701 | function modelConstruct(maybeInternal) { 702 | hideInternals(this); 703 | 704 | this.$key = null; 705 | this.$values = {}; 706 | this.$cas = null; 707 | this.$loaded = true; 708 | this.$initial = undefined; 709 | this.$cache = undefined; 710 | 711 | if (maybeInternal !== INTERNALGIZMO) { 712 | // For user-constructed objects, local cache! 713 | this.$cache = {}; 714 | 715 | if (this.$.constructor) { 716 | this.$constructing = true; 717 | this.$.constructor.apply(this, arguments); 718 | delete this.$constructing; 719 | } 720 | 721 | // Put myself in my own cache. 722 | var key = modelKey.call(this); 723 | this.$cache[key] = this; 724 | } 725 | } 726 | 727 | function modelDoValidation() { 728 | for (var i = 0; i < this.$.required.length; ++i) { 729 | if (!this[this.$.required[i]]) { 730 | throw new Error('required field missing: ' + this.$.required[i]); 731 | } 732 | } 733 | 734 | for (var i in this.$.schema) { 735 | if (this.$.schema.hasOwnProperty(i)) { 736 | var field = this.$.schema[i]; 737 | if (field.validator) { 738 | field.validator.check(this[i]); 739 | } 740 | } 741 | } 742 | } 743 | 744 | function modelKey() { 745 | if (!this.$key) { 746 | var key = this.$.name; 747 | for (var i = 0; i < this.$.id.length; ++i) { 748 | key += '_' + this[this.$.id[i]]; 749 | } 750 | this.$key = key.toLowerCase(); 751 | } 752 | return this.$key; 753 | } 754 | 755 | function findModelById() { 756 | var callback = arguments[arguments.length-1]; 757 | 758 | var info = this.prototype.$; 759 | 760 | var key = info.name; 761 | for (var i = 0; i < info.id.length; ++i) { 762 | key += '_' + arguments[i]; 763 | } 764 | key = key.toLowerCase(); 765 | 766 | info.bucket.get(key, {}, function(err, result) { 767 | if (err) { 768 | return callback(err); 769 | } 770 | 771 | var obj = obxToObj(result.value, info.name, null, 0, {}, key); 772 | if (obj.$.name != info.name) { 773 | throw new Error(obj.$.name + ' is not a ' + info.name); 774 | } 775 | 776 | callback(null, obj); 777 | }); 778 | } 779 | 780 | module.exports.key = function(obj) { 781 | return modelKey.call(obj); 782 | }; 783 | 784 | function findModelByRefDoc(con, refdoc, keys, callback) { 785 | var info = con.prototype.$; 786 | 787 | var refDocKey = _buildRefDocName(info.name, keys, refdoc); 788 | info.bucket.get(refDocKey, {}, function(err, result) { 789 | if (err) { 790 | return callback(err); 791 | } 792 | 793 | var docKey = result.value; 794 | info.bucket.get(docKey, {}, function(err, result) { 795 | if (err) { 796 | return callback(err); 797 | } 798 | 799 | var obj = obxToObj(result.value, info.name, null, 0, {}, docKey); 800 | if (obj.$.name != info.name) { 801 | throw new Error(obj.$.name + ' is not a ' + info.name); 802 | } 803 | 804 | for (var i = 0; i < refdoc.key.length; ++i) { 805 | var refField = refdoc.key[i]; 806 | if (obj[refField] !== keys[i]) { 807 | // This means that the refered document no longer matches up 808 | // with the reference that was used to find it... 809 | 810 | // TODO: Proper error here 811 | return callback('not_found', null); 812 | } 813 | } 814 | 815 | callback(null, obj); 816 | }); 817 | }); 818 | } 819 | 820 | 821 | 822 | 823 | 824 | 825 | function registerField(con, field, options) { 826 | var info = con.prototype.$; 827 | 828 | if (options.required) { 829 | info.required.push(field); 830 | } 831 | 832 | var getter = null; 833 | if (!options.auto) { 834 | getter = function() { 835 | return this.$values[options.name]; 836 | }; 837 | } else if(options.auto === 'uuid') { 838 | getter = function() { 839 | if (!this.$values[options.name]) { 840 | this.$values[options.name] = uuid.v4(); 841 | } 842 | return this.$values[options.name]; 843 | } 844 | } 845 | 846 | var setter = function(val) { 847 | if (!options.readonly || this.$constructing) { 848 | this.$values[options.name] = val; 849 | } else { 850 | throw new Error('attempted to set read-only property ' + field); 851 | } 852 | }; 853 | 854 | Object.defineProperty(con.prototype, field, { 855 | get: getter, 856 | set: setter 857 | }); 858 | } 859 | 860 | function modelQuery(query, options, callback) { 861 | var self = this; 862 | 863 | var viewQuery = couchbase.ViewQuery.from(query.ddoc, query.view) 864 | .key(this.$key) 865 | .limit(query.limit); 866 | 867 | viewQuery.sort = query.sort; 868 | 869 | query.bucket.query(viewQuery, function(err, results) { 870 | if (err) { 871 | return callback(err); 872 | } 873 | 874 | var resultObjs = []; 875 | for (var i = 0; i < results.length; ++i) { 876 | var obj = createRefObj(results[i].id, query.target, self.$cache); 877 | resultObjs[i] = obj; 878 | } 879 | 880 | callback(null, resultObjs); 881 | }); 882 | } 883 | 884 | function registerIndexX(con, name, options) { 885 | var info = con.prototype.$; 886 | 887 | if (options.type === 'refdoc') { 888 | info.refdocs.push(options); 889 | 890 | con[name] = function() { 891 | var callback = arguments[arguments.length-1]; 892 | var keys = []; 893 | for (var i = 0; i < arguments.length - 1; ++i) { 894 | keys.push(arguments[i]); 895 | } 896 | 897 | findModelByRefDoc(con, options, keys, callback); 898 | } 899 | } 900 | } 901 | 902 | /* 903 | target: 'BlogPost', 904 | mappedBy: 'creator', 905 | sort: 'desc', 906 | limit: 5 907 | */ 908 | function registerQuery(con, name, options) { 909 | var info = con.prototype.$; 910 | 911 | var query = {}; 912 | query.name = name; 913 | query.target = options.target; 914 | query.mappedBy = options.mappedBy; 915 | query.sort = options.sort ? options.sort : 'desc'; 916 | query.limit = options.limit ? options.limit : 0; 917 | query.bucket = info.bucket; 918 | 919 | info.queries[name] = query; 920 | queries.push(query); 921 | 922 | con.prototype[name] = function(options, callback) { 923 | if (!callback) { 924 | callback = options; 925 | options = {}; 926 | } 927 | 928 | modelQuery.call(this, query, options, callback); 929 | } 930 | } 931 | 932 | var indexes = []; 933 | 934 | function matchIndex(l, r) { 935 | if(l.bucket === r.bucket && l.type === r.type) { 936 | for (var j = 0; j < l.fields.length; ++j) { 937 | // Stop searching at the end of either field list 938 | if (j >= r.fields.length) break; 939 | 940 | // No match if a field doesnt match 941 | if (l.fields[j] !== r.fields[j]) { 942 | return false; 943 | } 944 | } 945 | return true; 946 | } 947 | return false; 948 | } 949 | 950 | function registerIndex(bucket, type, fields, options) { 951 | var newIndex = { 952 | bucket: bucket, 953 | type: type, 954 | fields: fields 955 | }; 956 | 957 | for (var i = 0; i < indexes.length; ++i) { 958 | var index = indexes[i]; 959 | 960 | if (matchIndex(newIndex, index)) { 961 | if (index.fields.length > fields.length) { 962 | // If the index is longer, we already have everything we need. 963 | } else { 964 | // If the newIndex is longer, use it instead. 965 | index.fields = fields; 966 | } 967 | 968 | return; 969 | } 970 | } 971 | 972 | indexes.push(newIndex); 973 | } 974 | 975 | function getIndexInfo(index) { 976 | var ddocName = '__ottoidx_' + index.type; 977 | var viewName = 'by'; 978 | for (var i = 0; i < index.fields.length; ++i) { 979 | viewName += '_' + index.fields[i]; 980 | } 981 | 982 | return [ddocName, viewName]; 983 | } 984 | 985 | function getIndexView(bucket, type, fields, options) { 986 | var searchIndex = { 987 | bucket: bucket, 988 | type: type, 989 | fields: fields 990 | }; 991 | 992 | for (var i = 0; i < indexes.length; ++i) { 993 | var index = indexes[i]; 994 | 995 | if (matchIndex(searchIndex, index)) { 996 | return getIndexInfo(index); 997 | } 998 | } 999 | 1000 | return false; 1001 | } 1002 | 1003 | function buildDesignDocs() { 1004 | // Register all indexes 1005 | for (var i = 0; i < queries.length; ++i) { 1006 | var query = queries[i]; 1007 | registerIndex(query.bucket, query.target, [query.mappedBy], {}); 1008 | } 1009 | 1010 | // Assign generated views 1011 | for (var i = 0; i < queries.length; ++i) { 1012 | var query = queries[i]; 1013 | var idxIfo = getIndexView(query.bucket, query.target, [query.mappedBy], {}); 1014 | query.ddoc = idxIfo[0]; 1015 | query.view = idxIfo[1]; 1016 | } 1017 | } 1018 | module.exports.buildDesignDocs = buildDesignDocs; 1019 | 1020 | function verifyDdocView(bucket, ddoc, view, callback) { 1021 | var timeoutErr = null; 1022 | setTimeout(function() { 1023 | timeoutErr = new Error('View creation timed out.'); 1024 | }, 8000); 1025 | 1026 | (function tryOnce() { 1027 | if (timeoutErr) { 1028 | return callback(timeoutErr); 1029 | } 1030 | 1031 | var viewQuery = couchbase.ViewQuery.from(ddoc, view).limit(0); 1032 | 1033 | bucket.query(viewQuery, function(err, results) { 1034 | if (err) { 1035 | return tryOnce(); 1036 | } 1037 | callback(null); 1038 | }); 1039 | })(); 1040 | } 1041 | 1042 | function setAndVerifyDdoc(bucket, ddoc, data, callback) { 1043 | bucket.manager().upsertDesignDocument(ddoc, data, function(err) { 1044 | if (err) { 1045 | return callback(err); 1046 | } 1047 | 1048 | var remaining = 0; 1049 | for (var i in data.views) { 1050 | remaining++; 1051 | if (data.views.hasOwnProperty(i)) { 1052 | verifyDdocView(bucket, ddoc, i, function(err) { 1053 | if (err) { 1054 | return callback(err); 1055 | } 1056 | 1057 | remaining--; 1058 | if (remaining == 0) { 1059 | callback(null); 1060 | } 1061 | }); 1062 | } 1063 | } 1064 | }); 1065 | } 1066 | 1067 | function registerDesignDocs(callback) { 1068 | // Force an immediate build 1069 | buildDesignDocs(); 1070 | 1071 | var buckets = []; 1072 | var views = []; 1073 | 1074 | for (var i = 0; i < indexes.length; ++i) { 1075 | var index = indexes[i]; 1076 | var idxIfo = getIndexInfo(index); 1077 | var typeInfo = typeList[index.type].prototype.$; 1078 | 1079 | var viewStrUp = []; 1080 | var viewStrDn = []; 1081 | 1082 | viewStrUp.push('function(doc,meta) {'); 1083 | viewStrDn.push('}'); 1084 | 1085 | for (var j in typeInfo.descrims) { 1086 | if (typeInfo.descrims.hasOwnProperty(j)) { 1087 | viewStrUp.push('if (doc.' + j + ' == \'' + typeInfo.descrims[j] + '\') {'); 1088 | viewStrDn.push('}'); 1089 | } 1090 | } 1091 | 1092 | var emitList = []; 1093 | for (var j = 0; j < index.fields.length; ++j) { 1094 | var field = index.fields[j]; 1095 | viewStrUp.push('if (doc.'+field+' && doc.'+field+'.$ref) {'); 1096 | viewStrDn.push('}'); 1097 | emitList.push('doc.' + field + '.$ref[1]'); 1098 | } 1099 | if (emitList.length > 1) { 1100 | viewStrUp.push('emit([' + emitList.join(',') + '],null);') 1101 | } else { 1102 | viewStrUp.push('emit(' + emitList[0] + ',null);'); 1103 | } 1104 | 1105 | var viewStr = viewStrUp.join('\n') + '\n'; 1106 | viewStr += viewStrDn.reverse().join('\n') + '\n'; 1107 | 1108 | buckets.push(index.bucket); 1109 | views.push({ 1110 | bucket: index.bucket, 1111 | ddoc: idxIfo[0], 1112 | name: idxIfo[1], 1113 | data: {map: viewStr} 1114 | }); 1115 | } 1116 | 1117 | var remaining = 0; 1118 | for (var i = 0; i < buckets.length; ++i) { 1119 | var ddocs = {}; 1120 | 1121 | for (var j = 0; j < views.length; ++j) { 1122 | var view = views[j]; 1123 | if (view.bucket !== buckets[i]) { 1124 | continue; 1125 | } 1126 | 1127 | if (!ddocs[view.ddoc]) { 1128 | ddocs[view.ddoc] = {}; 1129 | } 1130 | 1131 | if (!ddocs[view.ddoc].views) { 1132 | ddocs[view.ddoc].views = {}; 1133 | } 1134 | 1135 | ddocs[view.ddoc].views[view.name] = view.data; 1136 | } 1137 | 1138 | for (var j in ddocs) { 1139 | if (ddocs.hasOwnProperty(j)) { 1140 | remaining++; 1141 | setAndVerifyDdoc(buckets[i], j, ddocs[j], function(err) { 1142 | remaining--; 1143 | if (remaining === 0) { 1144 | if (err) { 1145 | return callback(err); 1146 | } 1147 | 1148 | // TODO: only returns the last error 1149 | callback(null); 1150 | } 1151 | }); 1152 | } 1153 | } 1154 | } 1155 | } 1156 | module.exports.registerDesignDocs = registerDesignDocs; 1157 | 1158 | function normalizeSchema(schema) { 1159 | var fieldAliases = {}; 1160 | 1161 | for (var i in schema) { 1162 | if (schema.hasOwnProperty(i)) { 1163 | if (typeof(schema[i]) === 'string' || typeof(schema[i]) === 'function') { 1164 | schema[i] = { 1165 | type: schema[i] 1166 | }; 1167 | } else if (typeof(schema[i]) !== 'object') { 1168 | throw new Error('expected schema fields to be strings or objects'); 1169 | } 1170 | 1171 | if (!schema[i].name) { 1172 | schema[i].name = i; 1173 | } 1174 | 1175 | if (typeof(schema[i].type) === 'function') { 1176 | schema[i].type = schema[i].type.name; 1177 | } 1178 | 1179 | if (TYPEALIASES[schema[i].type]) { 1180 | schema[i].type = TYPEALIASES[schema[i].type]; 1181 | } 1182 | 1183 | if (schema[i].validator) { 1184 | if (schema[i].validator instanceof Function) { 1185 | schema[i].validator = Validator.custom(schema[i].validator); 1186 | } 1187 | if (!(schema[i].validator instanceof Validator)) { 1188 | throw new Error('validator property must be of type Validator'); 1189 | } 1190 | } 1191 | 1192 | if (fieldAliases[schema[i].name]) { 1193 | throw new Error('multiple fields are assigned the same alias'); 1194 | } 1195 | fieldAliases[schema[i].name] = true; 1196 | 1197 | if (schema[i].auto) { 1198 | // force auto fields to readonly 1199 | schema[i].readonly = true; 1200 | 1201 | if (schema[i].auto === 'uuid') { 1202 | if (schema[i].type && schema[i].type !== 'string') { 1203 | throw new Error('uuid fields must be string typed'); 1204 | } 1205 | schema[i].type = 'string'; 1206 | } else { 1207 | throw new Error('unknown auto mode'); 1208 | } 1209 | } 1210 | } 1211 | } 1212 | } 1213 | 1214 | function validateSchemaIds(ids, schema) { 1215 | for (var i = 0; i < ids.length; ++i) { 1216 | var field = schema[ids[i]]; 1217 | 1218 | if (!field) { 1219 | throw new Error('id specified that is not in the schema'); 1220 | } 1221 | if (!field.readonly) { 1222 | throw new Error('id fields must be readonly'); 1223 | } 1224 | 1225 | // Force required on for id fields 1226 | schema[ids[i]].required = true; 1227 | } 1228 | } 1229 | 1230 | function registerType(name, type) { 1231 | if (typeList[name]) { 1232 | throw new Error('Type with the name ' + name + ' was already registered'); 1233 | } 1234 | typeList[name] = type; 1235 | } 1236 | 1237 | function hideInternals(con) { 1238 | var internalFields = ["$key", "$values", "$cas", "$loaded", "$initial", "$cache"]; 1239 | 1240 | for (var i = 0; i < internalFields.length; ++i) { 1241 | Object.defineProperty(con, internalFields[i], { 1242 | enumerable: false, 1243 | writable: true 1244 | }); 1245 | } 1246 | } 1247 | 1248 | function createModel(name, schema, options) { 1249 | 1250 | // Create a base function for the model. This is done so that the 1251 | // stack traces will have a nice name for developers to identify. 1252 | 1253 | var con = null; 1254 | eval('con = function ' + name + '() { modelConstruct.apply(this, arguments); }'); 1255 | 1256 | // This is simply to avoid the warning about not using this function 1257 | // as it is invoked via a string eval above, and never directly. 1258 | if (false) { modelConstruct(); } 1259 | 1260 | // info object holds all the model-specific data. 1261 | var info = {}; 1262 | con.prototype.$ = info; 1263 | Object.defineProperty(con.prototype, "$", { 1264 | enumerable: false, 1265 | writable: true 1266 | }); 1267 | 1268 | // Store some stuff for later! 1269 | info.model = con; 1270 | info.name = name; 1271 | info.schema = schema; 1272 | info.constructor = options.constructor; 1273 | info.bucket = options.bucket; 1274 | info.embed = options.embed; 1275 | info.required = []; 1276 | info.queries = {}; 1277 | info.refdocs = []; 1278 | 1279 | // Build the id list 1280 | // This must happen before schema normalization 1281 | if (options.id) { 1282 | if (!Array.isArray(options.id)) { 1283 | info.id = [options.id]; 1284 | } else { 1285 | info.id = options.id; 1286 | } 1287 | } else { 1288 | if (!schema['_id']) { 1289 | schema['_id'] = {auto: 'uuid'}; 1290 | } 1291 | info.id = ['_id']; 1292 | } 1293 | 1294 | if (options.descriminators) { 1295 | if (!(options.descriminators instanceof Object)) { 1296 | throw new Error('descriminators must be an object'); 1297 | } 1298 | info.descrims = options.descriminators; 1299 | } else { 1300 | info.descrims = {_type: name}; 1301 | } 1302 | 1303 | // Normalize Schema 1304 | normalizeSchema(schema); 1305 | validateSchemaIds(info.id, schema); 1306 | 1307 | for (var i in schema) { 1308 | if (schema.hasOwnProperty(i)) { 1309 | registerField(con, i, schema[i]); 1310 | } 1311 | } 1312 | 1313 | var queries = options.queries; 1314 | if (queries) { 1315 | for (var i in queries) { 1316 | if (queries.hasOwnProperty(i)) { 1317 | registerQuery(con, i, queries[i]); 1318 | } 1319 | } 1320 | } 1321 | 1322 | var indexes = options.indexes; 1323 | if (indexes) { 1324 | for (var i in indexes) { 1325 | if (indexes.hasOwnProperty(i)) { 1326 | registerIndexX(con, i, indexes[i]); 1327 | } 1328 | } 1329 | } 1330 | 1331 | con.findById = findModelById; 1332 | 1333 | // Define the util parser thingy 1334 | con.prototype.inspect = function() { 1335 | var outobj = {}; 1336 | if (this.$loaded) { 1337 | for (var i in schema) { 1338 | if (schema.hasOwnProperty(i)) { 1339 | outobj[i] = this[i]; 1340 | } 1341 | } 1342 | } else { 1343 | outobj.$key = this.$key; 1344 | outobj.$loaded = this.$loaded; 1345 | } 1346 | return util.inspect(outobj); 1347 | }; 1348 | 1349 | con.prototype.toJSON = function(include_private) { 1350 | var outobj = {}; 1351 | if (this.$loaded) { 1352 | for (var i in schema) { 1353 | if (schema.hasOwnProperty(i)) { 1354 | if (include_private || !schema[i].private) { 1355 | outobj[i] = this[i]; 1356 | } 1357 | } 1358 | } 1359 | } 1360 | return outobj; 1361 | }; 1362 | 1363 | registerType(name, con); 1364 | return con; 1365 | } 1366 | module.exports.model = createModel; 1367 | 1368 | function createType(name, schema, options) { 1369 | if (!options) { 1370 | options = {}; 1371 | } 1372 | options.embed = true; 1373 | 1374 | return createModel(name, schema, options); 1375 | } 1376 | module.exports.type = createType; 1377 | 1378 | 1379 | function Validator() { 1380 | this.funcs = []; 1381 | } 1382 | Validator.prototype.check = function(val) { 1383 | for (var i = 0; i < this.funcs.length; ++i) { 1384 | this.funcs[i](val); 1385 | } 1386 | }; 1387 | 1388 | Validator.prototype.match = function(regexp) { 1389 | return this.custom(function(val) { 1390 | if (!regexp.match(val)) { 1391 | throw new Error('expected value to match ' + regexp); 1392 | } 1393 | }); 1394 | }; 1395 | Validator.prototype.in = function(list) { 1396 | return this.custom(function(val) { 1397 | if (list.indexOf(val) < 0) { 1398 | throw new Error('expected value to be in list: ' + list.join(',')); 1399 | } 1400 | }); 1401 | }; 1402 | Validator.prototype.min = function(min) { 1403 | return this.custom(function(val) { 1404 | if (min !== null && val < min) { 1405 | throw new Error('expected value to not be less than ' + min); 1406 | } 1407 | }); 1408 | }; 1409 | Validator.prototype.max = function(max) { 1410 | return this.custom(function(val) { 1411 | if (max !== null && val > max) { 1412 | throw new Error('expected value to not be more than ' + max); 1413 | } 1414 | }); 1415 | }; 1416 | Validator.prototype.range = function(min, max) { 1417 | return this.min(min).max(max); 1418 | }; 1419 | Validator.prototype.custom = function(func) { 1420 | this.funcs.push(func); 1421 | return this; 1422 | }; 1423 | 1424 | Validator.match = function(regexp) { 1425 | return (new Validator()).match(regexp); }; 1426 | Validator.in = function(list) { 1427 | return (new Validator()).in(list); }; 1428 | Validator.min = function(min) { 1429 | return (new Validator()).min(min); }; 1430 | Validator.max = function(max) { 1431 | return (new Validator()).max(max); }; 1432 | Validator.range = function(min, max) { 1433 | return (new Validator()).range(min, max); }; 1434 | Validator.custom = function(func) { 1435 | return (new Validator()).custom(func); }; 1436 | 1437 | module.exports.Validator = Validator; 1438 | --------------------------------------------------------------------------------