├── .npmignore ├── index.js ├── .editorconfig ├── test ├── fixtures │ └── db.js └── unit │ ├── utils.test.js │ ├── schema.test.js │ ├── schematype.test.js │ ├── index.test.js │ └── model.test.js ├── .gitignore ├── package.json ├── gulpfile.js ├── README.md ├── lib ├── utils.js ├── index.js ├── schematype.js ├── schema.js └── model.js └── .jshintrc /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index'); 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines with a newline ending every file 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | 12 | [{package.json,.travis.yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /test/fixtures/db.js: -------------------------------------------------------------------------------- 1 | var kouch = require('../../lib/'); 2 | 3 | exports.before = function () { 4 | kouch.connect('couchbase://192.168.0.12', { name: 'default' }, null, true); 5 | }; 6 | 7 | exports.after = function () { 8 | kouch.disconnect(); 9 | }; 10 | 11 | //mocha hooks 12 | before(exports.before); 13 | after(exports.after); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sublime text 2 files 2 | *.sublime-* 3 | *.*~*.TMP 4 | 5 | # temp files 6 | .DS_Store 7 | Thumbs.db 8 | Desktop.ini 9 | npm-debug.log 10 | 11 | # vim swap files 12 | *.sw* 13 | 14 | # emacs temp files 15 | *~ 16 | \#*# 17 | 18 | # project ignores 19 | !.gitkeep 20 | *__temp 21 | .coverdata/ 22 | .coverrun 23 | build/ 24 | node_modules/ 25 | -------------------------------------------------------------------------------- /test/unit/utils.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | kouch = require('../../lib/'); 3 | 4 | // register our DB middleware 5 | require('../fixtures/db'); 6 | 7 | describe('Kouch.utils', function () { 8 | describe('#walk', function () { 9 | 10 | }); 11 | 12 | describe('#getValue', function () { 13 | 14 | }); 15 | 16 | describe('#setValue', function () { 17 | 18 | }); 19 | 20 | describe('#deepEqual', function () { 21 | 22 | }); 23 | 24 | describe('#options', function () { 25 | 26 | }); 27 | 28 | describe('#isObject', function () { 29 | 30 | }); 31 | 32 | describe('#clone', function () { 33 | 34 | }); 35 | 36 | /////// 37 | // Statics 38 | /////// 39 | 40 | describe('.compile', function () { 41 | 42 | }); 43 | 44 | describe('.toJSON', function () { 45 | 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kouch", 3 | "version": "0.1.0", 4 | "description": "Kouch is an ODM for couchase.", 5 | "homepage": "https://github.com/englercj/kouch", 6 | "author": "Chad Engler ", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "gulp test", 11 | "testci": "gulp testci" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/englercj/kouch.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/englercj/kouch/issues" 19 | }, 20 | "dependencies": { 21 | "couchbase": "^2.0.6", 22 | "hooks": "^0.3.2", 23 | "regexp-clone": "^0.0.1", 24 | "sliced": "^0.0.5", 25 | "uuid": "^2.0.1" 26 | }, 27 | "devDependencies": { 28 | "async": "^0.9.0", 29 | "chai": "^1.10.0", 30 | "gulp": "^3.8.10", 31 | "gulp-coverage": "^0.3.32", 32 | "gulp-jshint": "^1.9.0", 33 | "gulp-mocha": "^2.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/schema.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | kouch = require('../../lib/'); 3 | 4 | // register our DB middleware 5 | require('../fixtures/db'); 6 | 7 | describe('Kouch.Schema', function () { 8 | describe('#ctor', function () { 9 | 10 | }); 11 | 12 | describe('#defaultOptions', function () { 13 | 14 | }); 15 | 16 | describe('#add', function () { 17 | 18 | }); 19 | 20 | describe('#path', function () { 21 | 22 | }); 23 | 24 | describe('#virtualpath', function () { 25 | 26 | }); 27 | 28 | describe('#pathType', function () { 29 | 30 | }); 31 | 32 | describe('#queue', function () { 33 | 34 | }); 35 | 36 | describe('#pre', function () { 37 | 38 | }); 39 | 40 | describe('#post', function () { 41 | 42 | }); 43 | 44 | describe('#method', function () { 45 | 46 | }); 47 | 48 | describe('#static', function () { 49 | 50 | }); 51 | 52 | describe('#set', function () { 53 | 54 | }); 55 | 56 | describe('#get', function () { 57 | 58 | }); 59 | 60 | describe('#virtual', function () { 61 | 62 | }); 63 | 64 | /////// 65 | // Statics 66 | /////// 67 | 68 | describe('.reserved', function () { 69 | 70 | }); 71 | 72 | describe('.interpretAsType', function () { 73 | 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | mocha = require('gulp-mocha'), 3 | cover = require('gulp-coverage'), 4 | jshint = require('gulp-jshint'); 5 | 6 | /***** 7 | * JSHint task, lints the lib and test *.js files. 8 | *****/ 9 | gulp.task('jshint', function () { 10 | return gulp.src(['./lib/**/*.js', './test/**/*.js']) 11 | .pipe(jshint()) 12 | .pipe(jshint.reporter('default')); 13 | }); 14 | 15 | /***** 16 | * Test task, runs mocha against unit test files. 17 | *****/ 18 | gulp.task('test', function () { 19 | return gulp.src('./test/unit/**/*.test.js', { read: false }) 20 | .pipe(mocha({ 21 | ui: 'bdd', 22 | reporter: 'spec' 23 | })); 24 | }); 25 | 26 | /***** 27 | * Coverage task, runs mocha tests and covers the lib files. 28 | *****/ 29 | gulp.task('cover', function () { 30 | return gulp.src('./test/unit/**/*.test.js', { read: false }) 31 | .pipe(cover.instrument({ 32 | pattern: ['./lib/*.js'] 33 | })) 34 | .pipe(mocha({ 35 | ui: 'bdd', 36 | reporter: 'spec', 37 | timeout: 30000 38 | })) 39 | .pipe(cover.gather()) 40 | .pipe(cover.format()) 41 | .pipe(gulp.dest('./.coverdata')); 42 | }); 43 | 44 | /***** 45 | * Default task, runs jshint and test tasks. 46 | *****/ 47 | gulp.task('default', ['jshint', 'test']); 48 | -------------------------------------------------------------------------------- /test/unit/schematype.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | kouch = require('../../lib/'), 3 | TestSchema = new kouch.Schema({ 4 | fieldRequired: { type: String, required: true }, 5 | fieldMin: { type: Number, min: 8 }, 6 | fieldMax: { type: Number, max: 22 }, 7 | fieldMatch: { type: String, match: /[a-zA-z]+/ }, 8 | fieldEnum: { type: String, enum: ['hey', 'there', 'you'] } 9 | }), 10 | TestModel = kouch.model('SchemaTypeTest', 'default', TestSchema); 11 | 12 | // register our DB middleware 13 | require('../fixtures/db'); 14 | 15 | describe('Kouch.SchemaType', function () { 16 | describe('#ctor', function () { 17 | 18 | }); 19 | 20 | describe('#cast', function () { 21 | 22 | }); 23 | 24 | describe('#get', function () { 25 | 26 | }); 27 | 28 | describe('#set', function () { 29 | 30 | }); 31 | 32 | describe('#default', function () { 33 | 34 | }); 35 | 36 | describe('#validate', function () { 37 | 38 | }); 39 | 40 | describe('#getDefault', function () { 41 | 42 | }); 43 | 44 | /////// 45 | // Setter Shortcuts 46 | /////// 47 | 48 | describe('#lowercase', function () { 49 | 50 | }); 51 | 52 | describe('#uppercase', function () { 53 | 54 | }); 55 | 56 | describe('#trim', function () { 57 | 58 | }); 59 | 60 | /////// 61 | // Validator shortcuts 62 | /////// 63 | 64 | describe('#required', function () { 65 | it('Should fail validation if a required field is missing', function (done) { 66 | var m = new TestModel({ fieldMin: 10, fieldMax: 20, fieldMatch: 'abc', fieldEnum: 'hey' }); 67 | 68 | m.save(function (err) { 69 | expect(err).to.be.an.instanceOf(Error); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('#min', function () { 76 | 77 | }); 78 | 79 | describe('#max', function () { 80 | 81 | }); 82 | 83 | describe('#match', function () { 84 | 85 | }); 86 | 87 | describe('#enum', function () { 88 | 89 | }); 90 | 91 | /////// 92 | // Apply functions 93 | /////// 94 | 95 | describe('#applyGetters', function () { 96 | 97 | }); 98 | 99 | describe('#applySetters', function () { 100 | 101 | }); 102 | 103 | describe('#applyValidators', function () { 104 | 105 | }); 106 | 107 | /////// 108 | // Statics 109 | /////// 110 | 111 | describe('.compile', function () { 112 | 113 | }); 114 | 115 | describe('.toJSON', function () { 116 | 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kouch 2 | 3 | Couchbase ODM heavily inspired by [`mongoose`][0]. 4 | 5 | **WARNING:** Still heavily in development, incomplete, and only lightly tested. 6 | 7 | [0]: https://github.com/LearnBoost/mongoose 8 | 9 | ## Example 10 | 11 | Here is an example of a `User` model that is stored in the `default` bucket and shows off many of the features of kouch: 12 | 13 | ```javascript 14 | // create the base schema 15 | UserSchema = new kouch.Schema({ 16 | name: { 17 | first: String, 18 | last: String 19 | }, 20 | email: { type: String, key: true}, 21 | password: String, 22 | age: { type: Number, min: 18 }, 23 | bio: { type: String, match: /[\w\d]+/, trim: true }, 24 | date: { type: Date, default: Date.now } 25 | }); 26 | 27 | // set the key prefix for when a comment is stored 28 | UserSchema.set('key.prefix', 'user:account:'); 29 | 30 | // a setter for the first name that capitalizes it 31 | UserSchema.path('name.first').set(function (val) { 32 | return val.substr(0, 1).toUpperCase() + val.substr(1); 33 | }); 34 | 35 | // middleware 36 | UserSchema.pre('save', function (next) { 37 | var user = this; 38 | 39 | // only hash the password if the password has changed 40 | if (!this.isModified('password')) { 41 | return next(); 42 | } 43 | 44 | bcrypt.hash(user.password, 8, function(err, hash) { 45 | if (err) return next(err); 46 | 47 | user.password = hash; 48 | next(); 49 | }); 50 | }); 51 | 52 | // instance methods on an instance of the model created from this schema 53 | // will be accessible at: (new UserModel()).x 54 | UserSchema.methods.x = function (cb) { 55 | // need a good example here... 56 | }; 57 | 58 | // static method on the model created from this schema 59 | // will be accessible at: UserModel.authenticate 60 | UserSchema.statics.authenticate = function(email, password, cb) { 61 | this.load(email, function (err, user) { 62 | if (err) return cb(err); 63 | 64 | bcrypt.compare(password, user.password, function (err, isMatch) { 65 | if (err) return cb(err); 66 | if (!isMatch) return cb(new Error('Your email and password combination is invalid.')); 67 | 68 | cb(null, user); 69 | }); 70 | }); 71 | }; 72 | 73 | // virtual property, not persisted to the DB 74 | UserSchema.virtual('name.full') 75 | .get(function () { 76 | return this.name.first + ' ' + this.name.last; 77 | }) 78 | .set(function (name) { 79 | var split = name.split(' '); 80 | this.name.first = split[0]; 81 | this.name.last = split[1]; 82 | }); 83 | 84 | 85 | // create the model from the schema 86 | var UserModel = kouch.model('User', 'default', UserSchema); 87 | 88 | // create an instance of the model with data 89 | var user = new UserModel({ 90 | name: { 91 | first: 'Chad', 92 | last: 'Engler' 93 | }, 94 | email: 'me@somewhere.com', 95 | password: 'secret', 96 | age: 21, 97 | bio: 'I am a person!' 98 | }); 99 | 100 | 101 | // save the user 102 | user.save(); 103 | ``` 104 | 105 | ## License 106 | 107 | The MIT License (MIT) 108 | 109 | Copyright (c) 2014 Chad Engler 110 | 111 | Permission is hereby granted, free of charge, to any person obtaining a copy 112 | of this software and associated documentation files (the "Software"), to deal 113 | in the Software without restriction, including without limitation the rights 114 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 115 | copies of the Software, and to permit persons to whom the Software is 116 | furnished to do so, subject to the following conditions: 117 | 118 | The above copyright notice and this permission notice shall be included in 119 | all copies or substantial portions of the Software. 120 | 121 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 122 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 123 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 124 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 125 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 126 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 127 | THE SOFTWARE. 128 | -------------------------------------------------------------------------------- /test/unit/index.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | kouch = require('../../lib/'); 3 | 4 | // register our DB middleware 5 | require('../fixtures/db'); 6 | 7 | describe('Kouch', function () { 8 | it('Should export the proper functions', function () { 9 | // exported data 10 | expect(kouch.buckets).to.be.an('object'); 11 | expect(kouch.models).to.be.an('object'); 12 | expect(kouch.options).to.be.an('object'); 13 | expect(kouch.cluster).to.be.an('object'); 14 | 15 | // instance methods 16 | expect(kouch.set).to.be.a('function'); 17 | expect(kouch.get).to.be.a('function'); 18 | expect(kouch.connect).to.be.a('function'); 19 | expect(kouch.disconnect).to.be.a('function'); 20 | expect(kouch.openBucket).to.be.a('function'); 21 | expect(kouch.model).to.be.a('function'); 22 | 23 | // exported prototype objects 24 | expect(kouch.Schema).to.be.a('function'); 25 | }); 26 | 27 | describe('#set, #get', function () { 28 | it('Should set/get the proper option property', function () { 29 | expect( 30 | kouch.set('key.prefix', 'testing').get('key.prefix') 31 | ) 32 | .to.equal('testing'); 33 | }); 34 | }); 35 | 36 | describe('#openBucket', function () { 37 | it('Should return the same bucket if the name already exists'); 38 | 39 | it('Should create a new bucket connection'); 40 | }); 41 | 42 | describe('#model', function () { 43 | it('Should create a schema instance for you when a plain object is passed'); 44 | 45 | it('Should return the model if it has been created already'); 46 | 47 | it('Should throw an error when there is already a model and a different schema is passed'); 48 | 49 | it('Should compile a new model for each bucket/name pair'); 50 | }); 51 | 52 | describe('general use', function () { 53 | it('General case usage', function () { 54 | var getDate = function (val) { 55 | return (val.getMonth() + 1) + '/' + val.getDate() + '/' + val.getFullYear(); 56 | }; 57 | 58 | var CommentSchema = new kouch.Schema({ 59 | name: { 60 | first: { type: String, default: 'first' }, 61 | last: String 62 | }, 63 | age : { type: Number, min: 18 }, 64 | bio : { type: String, match: /[a-z]/ }, 65 | date: { type: Date, default: Date.now, get: getDate }, 66 | buff: Buffer, 67 | body: String 68 | }); 69 | 70 | // set some options on how the key is constructed 71 | CommentSchema.set('key.prefix', 'comments:'); 72 | 73 | // a setter 74 | CommentSchema.path('name.first').set(function (val) { 75 | return val.substr(0, 1).toUpperCase() + val.substr(1); 76 | }); 77 | 78 | // a getter 79 | CommentSchema.path('bio').get(function (val) { 80 | return val.trim(); 81 | }); 82 | 83 | // middleware 84 | CommentSchema.pre('save', function (next) { 85 | this.name.first += '-pre-save'; 86 | // notify(this.get('email')); 87 | expect(this.name.first).to.equal('Brittany-pre-save'); 88 | next(); 89 | }); 90 | 91 | // instance methods on a created model 92 | CommentSchema.methods.findSimilarTypes = function (/* cb */) { 93 | // return this.model('Animal').find({ type: this.type }, cb); 94 | }; 95 | 96 | // static method on the model created from this schema 97 | CommentSchema.statics.findByName = function (/* name, cb */) { 98 | // this.find({ name: new RegExp(name, 'i') }, cb); 99 | }; 100 | 101 | // virtual property that is not persisted to the DB 102 | CommentSchema.virtual('name.full') 103 | .get(function () { 104 | return this.name.first + ' ' + this.name.last; 105 | }) 106 | .set(function (name) { 107 | var split = name.split(' '); 108 | this.name.first = split[0]; 109 | this.name.last = split[1]; 110 | }); 111 | 112 | 113 | // create the model from the schema 114 | var CommentModel = kouch.model('Comment', 'default', CommentSchema); 115 | 116 | var doc, 117 | comment = new CommentModel(doc = { 118 | name: { 119 | first: 'Chad', 120 | last: 'Engler' 121 | }, 122 | age: 21, 123 | bio: 'I am a person!', 124 | buff: new Buffer(16), 125 | body: 'Comment body!' 126 | }); 127 | 128 | // check schema options 129 | expect(CommentSchema.get('key.prefix')).to.equal('comments:'); 130 | 131 | // check document getters 132 | expect(comment.name.first).to.equal(doc.name.first); 133 | expect(comment.name.last).to.equal(doc.name.last); 134 | expect(comment.age).to.equal(doc.age); 135 | expect(comment.bio).to.equal(doc.bio); 136 | expect(comment.date).to.be.equal(getDate(new Date())); //getter modifies the value 137 | expect(comment.buff).to.equal(doc.buff); 138 | expect(comment.body).to.equal(doc.body); 139 | expect(comment._id).to.be.a('string'); 140 | 141 | // check bio getter 142 | comment.bio = ' space '; 143 | expect(comment.bio).to.equal('space'); 144 | 145 | // check methods 146 | expect(comment.findSimilarTypes).to.be.a('function'); 147 | 148 | // check statics 149 | expect(CommentModel.findByName).to.be.a('function'); 150 | 151 | // check virtual getter 152 | expect(comment.name.full).to.equal('Chad Engler'); 153 | 154 | // check setter method 155 | comment.name.first = 'john'; 156 | expect(comment.name.first).to.equal('John'); //setter modifies the value 157 | 158 | // check virtual setter 159 | comment.name.full = 'Brittany Engler'; 160 | expect(comment.name.first).to.equal('Brittany'); 161 | expect(comment.name.last).to.equal('Engler'); 162 | expect(comment.name.full).to.equal('Brittany Engler'); 163 | 164 | // check saving 165 | comment.save(); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var cloneRegExp = require('regexp-clone'); 2 | 3 | /** 4 | * A faster Array.prototype.slice.call(arguments) alternative 5 | * 6 | */ 7 | exports.args = require('sliced'); 8 | 9 | exports.walk = function (path, obj, cb) { 10 | var steps = path.split('.'); 11 | 12 | for (var i = 0; i < steps.length - 1; ++i) { 13 | obj = obj[steps[i]]; 14 | } 15 | 16 | cb(obj, steps[steps.length - 1]); 17 | }; 18 | 19 | exports.getValue = function (path, obj) { 20 | var ret; 21 | 22 | exports.walk(path, obj, function (o, k) { 23 | ret = o[k]; 24 | }); 25 | 26 | return ret; 27 | }; 28 | 29 | exports.setValue = function (path, obj, value) { 30 | exports.walk(path, obj, function (o, k) { 31 | o[k] = value; 32 | }); 33 | 34 | return value; 35 | }; 36 | 37 | /** 38 | * Determines if `a` and `b` are deep equal. 39 | * 40 | * Modified from node/lib/assert.js 41 | * 42 | * @method deepEqual 43 | * @param a {mixed} a value to compare to `b` 44 | * @param b {mixed} a value to compare to `a` 45 | * @return {Boolean} 46 | */ 47 | exports.deepEqual = function (a, b) { 48 | if (a === b) return true; 49 | 50 | if (a instanceof Date && b instanceof Date) 51 | return a.getTime() === b.getTime(); 52 | 53 | // if (a instanceof ObjectId && b instanceof ObjectId) 54 | // return a.toString() === b.toString(); 55 | 56 | if (a instanceof RegExp && b instanceof RegExp) { 57 | return a.source === b.source && 58 | a.ignoreCase === b.ignoreCase && 59 | a.multiline === b.multiline && 60 | a.global === b.global; 61 | } 62 | 63 | if (typeof a !== 'object' && typeof b !== 'object') 64 | return a === b; 65 | 66 | if (a === null || b === null || a === undefined || b === undefined) 67 | return false; 68 | 69 | if (a.prototype !== b.prototype) return false; 70 | 71 | // Handle MongooseNumbers 72 | if (a instanceof Number && b instanceof Number) { 73 | return a.valueOf() === b.valueOf(); 74 | } 75 | 76 | if (Buffer.isBuffer(a)) { 77 | return exports.buffer.areEqual(a, b); 78 | } 79 | 80 | // if (isMongooseObject(a)) a = a.toObject(); 81 | // if (isMongooseObject(b)) b = b.toObject(); 82 | 83 | var ka, kb, key, i; 84 | 85 | try { 86 | ka = Object.keys(a); 87 | kb = Object.keys(b); 88 | } catch (e) {//happens when one is a string literal and the other isn't 89 | return false; 90 | } 91 | 92 | // having the same number of owned properties (keys incorporates 93 | // hasOwnProperty) 94 | if (ka.length !== kb.length) 95 | return false; 96 | 97 | // the same set of keys (although not necessarily the same order), 98 | ka.sort(); 99 | kb.sort(); 100 | 101 | // cheap key test 102 | for (i = ka.length - 1; i >= 0; i--) { 103 | if (ka[i] !== kb[i]) 104 | return false; 105 | } 106 | 107 | // equivalent values for every corresponding key, and 108 | // possibly expensive deep test 109 | for (i = ka.length - 1; i >= 0; i--) { 110 | key = ka[i]; 111 | if (!exports.deepEqual(a[key], b[key])) return false; 112 | } 113 | 114 | return true; 115 | }; 116 | 117 | /** 118 | * Shallow copies defaults into options. 119 | * 120 | * @method options 121 | * @param options {Object} 122 | * @param defaults {Object} 123 | * @return {Object} the merged object 124 | */ 125 | exports.options = function (options, defaults) { 126 | var keys = Object.keys(defaults), 127 | i = keys.length, 128 | k; 129 | 130 | options = options || {}; 131 | 132 | while (i--) { 133 | k = keys[i]; 134 | if (!(k in options)) { 135 | options[k] = defaults[k]; 136 | } 137 | } 138 | 139 | return options; 140 | }; 141 | 142 | /** 143 | * Determines if `arg` is an object. 144 | * 145 | * @method isObject 146 | * @param arg {mixed} The argument to check. 147 | * @return {Boolean} 148 | */ 149 | exports.isObject = function (arg) { 150 | return Object.prototype.toString.call(arg) === '[object Object]'; 151 | }; 152 | 153 | function cloneObject(obj, options) { 154 | var retainKeyOrder = options && options.retainKeyOrder, 155 | minimize = options && options.minimize, 156 | ret = {}, 157 | hasKeys, 158 | keys, 159 | val, 160 | k, 161 | i; 162 | 163 | if (retainKeyOrder) { 164 | for (k in obj) { 165 | val = exports.clone(obj[k], options); 166 | 167 | if (!minimize || ('undefined' !== typeof val)) { 168 | hasKeys || (hasKeys = true); 169 | ret[k] = val; 170 | } 171 | } 172 | } else { 173 | // faster 174 | 175 | keys = Object.keys(obj); 176 | i = keys.length; 177 | 178 | while (i--) { 179 | k = keys[i]; 180 | val = exports.clone(obj[k], options); 181 | 182 | if (!minimize || ('undefined' !== typeof val)) { 183 | if (!hasKeys) hasKeys = true; 184 | ret[k] = val; 185 | } 186 | } 187 | } 188 | 189 | return minimize ? hasKeys && ret : ret; 190 | } 191 | 192 | function cloneArray(arr, options) { 193 | var ret = []; 194 | for (var i = 0, l = arr.length; i < l; i++) 195 | ret.push(exports.clone(arr[i], options)); 196 | 197 | return ret; 198 | } 199 | 200 | /** 201 | * Object clone. 202 | * 203 | * If options.minimize is true, creates a minimal data object. Empty objects and undefined 204 | * values will not be cloned. This makes the data payload sent to Couchbase as small as possible. 205 | * 206 | * Functions are never cloned. 207 | * 208 | * @param {Object} obj the object to clone 209 | * @param {Object} options 210 | * @return {Object} the cloned object 211 | * @api private 212 | */ 213 | exports.clone = function (obj, options) { 214 | if (obj === undefined || obj === null) 215 | return obj; 216 | 217 | if (Array.isArray(obj)) { 218 | return cloneArray(obj, options); 219 | } 220 | 221 | if (obj.constructor) { 222 | switch (obj.constructor.name) { 223 | case 'Object': 224 | return cloneObject(obj, options); 225 | case 'Date': 226 | return new obj.constructor(+obj); 227 | case 'RegExp': 228 | return cloneRegExp(obj); 229 | default: 230 | // ignore 231 | break; 232 | } 233 | } 234 | 235 | if (!obj.constructor && exports.isObject(obj)) { 236 | // object created with Object.create(null) 237 | return cloneObject(obj, options); 238 | } 239 | 240 | if (obj.valueOf) { 241 | return obj.valueOf(); 242 | } 243 | }; 244 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var couchbase = require('couchbase'), 2 | utils = require('./utils'), 3 | Schema = require('./schema'), 4 | Model = require('./model'); 5 | 6 | function Kouch() { 7 | this.cluster = null; 8 | this.buckets = {}; 9 | this.models = {}; 10 | 11 | this.cb = couchbase; 12 | 13 | this.defaultBucket = null; 14 | 15 | this._loadedProps = false; 16 | 17 | this.options = {}; 18 | } 19 | 20 | /** 21 | * Sets mongoose options 22 | * 23 | * ####Example: 24 | * 25 | * kouch.set('debug', true) // enable logging collection methods + arguments to the console 26 | * 27 | * @param key {String} 28 | * @param value {mixed} 29 | * @api public 30 | */ 31 | Kouch.prototype.set = function (key, value) { 32 | if (arguments.length === 1) { 33 | return this.options[key]; 34 | } 35 | 36 | this.options[key] = value; 37 | return this; 38 | }; 39 | 40 | Kouch.prototype.get = Kouch.prototype.set; 41 | 42 | /** 43 | * Opens a connection to a cluster. 44 | * 45 | * @method connect 46 | * @param dsn {String|Object} The connection string or an object with the proper properties. 47 | * @param [buckets] {String|Object|Array} The bucket connection object (name/password). 48 | * @param [callback] {Function} The callback to call when the bucket has opened. 49 | * @return {couchbase.Bucket} 50 | */ 51 | Kouch.prototype.connect = function (dsn, buckets, cb, mock) { 52 | if (mock) { 53 | this.cluster = new couchbase.Mock.Cluster(dsn); 54 | } 55 | else { 56 | this.cluster = new couchbase.Cluster(dsn); 57 | } 58 | 59 | 60 | // nothing passed, assume they want the default bucket 61 | if (!buckets) { 62 | buckets = { name: 'default' }; 63 | } 64 | // string bucket name passed, parse into object 65 | else if (typeof buckets === 'string') { 66 | buckets = { name: buckets }; 67 | } 68 | 69 | // catch the case of something strange being passed in... 70 | if (typeof buckets !== 'object') { 71 | throw new TypeError('Connection bucket must be a string, object { name, password }, or array of strings/objects.'); 72 | } 73 | 74 | // if an array is passed, open each one. 75 | if (buckets.length) { 76 | for (var i = 0; i < buckets.length; ++i) { 77 | var b = buckets[i]; 78 | 79 | if (typeof b === 'string') { 80 | this.openBucket(b, null, cb); 81 | } else { 82 | this.openBucket(b.name, b.password, cb); 83 | } 84 | } 85 | } 86 | // assume it is an object of form { name, password } 87 | else { 88 | this.openBucket(buckets.name, buckets.password, cb); 89 | } 90 | 91 | return this.cluster; 92 | }; 93 | 94 | /** 95 | * Disconnect from a specific bucket or from all buckets. 96 | * 97 | * @method disconnect 98 | * @param [name] {String} The name of the bucket to disconnect from, if none is passed all buckets are disconnected. 99 | */ 100 | Kouch.prototype.disconnect = function (name) { 101 | var buckets = this.buckets; 102 | 103 | if (name) { 104 | buckets[name].disconnect(); 105 | } else { 106 | Object.keys(buckets).forEach(function (name) { 107 | buckets[name].disconnect(); 108 | }); 109 | } 110 | }; 111 | 112 | /** 113 | * Opens a connection to a new bucket on the cluster. 114 | * 115 | * @method openBucket 116 | * @param name {String} The string name of the bucket. 117 | * @param password {String} The password to use to login. 118 | * @param [callback] {Function} The callback to call when the bucket has opened. 119 | * @return {couchbase.Bucket} 120 | */ 121 | Kouch.prototype.openBucket = function (name, password, cb) { 122 | if (!this.buckets[name]) { 123 | this.buckets[name] = this.cluster.openBucket(name, password, cb); 124 | } 125 | 126 | if (!this.defaultBucket) { 127 | this.use(name); 128 | } 129 | 130 | return this.buckets[name]; 131 | }; 132 | 133 | /** 134 | * Creates a model based on a schema for a certain bucket. 135 | * 136 | * @method model 137 | * @param name {String} The string name of the model. 138 | * @param bucket {String} The name of the bucket to use. 139 | * @param schema {Schema|Object} The schema object to use. 140 | * @return {Model} 141 | */ 142 | // TODO: Creating a model before connecting causes the model's "bucket" to be undefined because 143 | // this.buckets is empty at this point (because none are connected). 144 | Kouch.prototype.model = function (name, bucket, schema) { 145 | if (utils.isObject(schema) && !(schema instanceof Schema)) { 146 | schema = new Schema(schema); 147 | } 148 | 149 | if (!this.models[name]) { 150 | this.models[name] = {}; 151 | } 152 | 153 | // if we have a model for this name/bucket then return it 154 | if (this.models[name][bucket]) { 155 | if (schema instanceof Schema && schema !== this.models[name][bucket].schema) { 156 | throw new Error( 157 | 'Passed schema does not match a previously create model for: ' + name + ', in bucket: ' + bucket 158 | ); 159 | } 160 | 161 | return this.models[name][bucket]; 162 | } 163 | 164 | // create a new model for this bucket and save it 165 | this.models[name][bucket] = Model.compile(name, bucket, this.buckets[bucket], schema, this); 166 | return this.models[name][bucket]; 167 | }; 168 | 169 | /** 170 | * The name of the bucket to use for default ops 171 | * 172 | * @param bucketName {string} 173 | */ 174 | Kouch.prototype.use = function (bucketName) { 175 | this.defaultBucket = this.buckets[bucketName]; 176 | }; 177 | 178 | // methods of default bucket 179 | [ 180 | 'query', 'get', 'getMulti', 'getAndTouch', 'getAndLock', 'getReplica', 181 | 'touch', 'unlock', 'remove', 'upsert', 'insert', 'replace', 'append', 182 | 'prepend', 'counter' 183 | ].forEach(function (key) { 184 | Object.defineProperty(Kouch.prototype, key, { 185 | get: methodWrapper(key) 186 | }); 187 | }); 188 | 189 | // properties of default bucket 190 | [ 191 | 'operationTimeout', 'viewTimeout', 'durabilityTimeout', 'durabilityInterval', 192 | 'managementTimeout', 'configThrottle', 'connectionTimeout', 'nodeConnectionTimeout' 193 | ].forEach(function (key) { 194 | Object.defineProperty(Kouch.prototype, key, { 195 | get: propertyGetWrapper(key), 196 | set: propertySetWrapper(key) 197 | }); 198 | }); 199 | 200 | function methodWrapper(key) { 201 | return function () { 202 | if (this.defaultBucket) { 203 | return this.defaultBucket[key].apply(this.defaultBucket, arguments); 204 | } 205 | } 206 | } 207 | 208 | function propertyGetWrapper(key) { 209 | return function () { 210 | if (this.defaultBucket) { 211 | return this.defaultBucket[key]; 212 | } 213 | } 214 | } 215 | function propertySetWrapper(key) { 216 | return function (value) { 217 | if (this.defaultBucket) { 218 | return this.defaultBucket[key] = value; 219 | } 220 | } 221 | } 222 | 223 | // Exports 224 | Kouch.prototype.Schema = Schema; 225 | module.exports = new Kouch(); 226 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Configuration 4 | // -------------------------------------------------------------------- 5 | // 6 | // @author Chad Engler 7 | 8 | // == Enforcing Options =============================================== 9 | // 10 | // These options tell JSHint to be more strict towards your code. Use 11 | // them if you want to allow only a safe subset of JavaScript, very 12 | // useful when your codebase is shared with a big number of developers 13 | // with different skill levels. 14 | 15 | "bitwise" : false, // Disallow bitwise operators (&, |, ^, etc.). 16 | "camelcase" : true, // Force all variable names to use either camelCase or UPPER_CASE. 17 | "curly" : false, // Require {} for every new block or scope. 18 | "eqeqeq" : true, // Require triple equals i.e. `===`. 19 | "es3" : false, // Enforce conforming to ECMAScript 3. 20 | "forin" : false, // Disallow `for in` loops without `hasOwnPrototype`. 21 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 22 | "indent" : 4, // Require that 4 spaces are used for indentation. 23 | "latedef" : true, // Prohibit variable use before definition. 24 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 25 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 26 | "noempty" : true, // Prohibit use of empty blocks. 27 | "nonew" : true, // Prohibit use of constructors for side-effects. 28 | "plusplus" : false, // Disallow use of `++` & `--`. 29 | "quotmark" : true, // Force consistency when using quote marks. 30 | "undef" : true, // Require all non-global variables be declared before they are used. 31 | "unused" : true, // Warn when varaibles are created by not used. 32 | "strict" : false, // Require `use strict` pragma in every file. 33 | "trailing" : true, // Prohibit trailing whitespaces. 34 | "maxparams" : 6, // Prohibit having more than X number of params in a function. 35 | "maxdepth" : 6, // Prohibit nested blocks from going more than X levels deep. 36 | "maxstatements" : false, // Restrict the number of statements in a function. 37 | "maxcomplexity" : false, // Restrict the cyclomatic complexity of the code. 38 | "maxlen" : 120, // Require that all lines are 100 characters or less. 39 | "predef" : [ // Register predefined globals used throughout the code. 40 | // Predef Mocha BDD interface 41 | "describe", "it", "before", "beforeEach", "after", "afterEach" 42 | ], 43 | 44 | // == Relaxing Options ================================================ 45 | // 46 | // These options allow you to suppress certain types of warnings. Use 47 | // them only if you are absolutely positive that you know what you are 48 | // doing. 49 | 50 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 51 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 52 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 53 | "eqnull" : false, // Tolerate use of `== null`. 54 | "esnext" : false, // Allow ES.next specific features such as `const` and `let`. 55 | "evil" : false, // Tolerate use of `eval`. 56 | "expr" : true, // Tolerate `ExpressionStatement` as Programs. 57 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 58 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 59 | "iterator" : false, // Allow usage of __iterator__ property. 60 | "lastsemic" : false, // Tolerate missing semicolons when the it is omitted for the last statement in a one-line block. 61 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 62 | "laxcomma" : false, // Suppress warnings about comma-first coding style. 63 | "loopfunc" : false, // Allow functions to be defined within loops. 64 | "moz" : false, // Code that uses Mozilla JS extensions will set this to true 65 | "multistr" : false, // Tolerate multi-line strings. 66 | "proto" : true, // Tolerate __proto__ property. This property is deprecated. 67 | "scripturl" : false, // Tolerate script-targeted URLs. 68 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 69 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 70 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 71 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 72 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 73 | 74 | // == Environments ==================================================== 75 | // 76 | // These options pre-define global variables that are exposed by 77 | // popular JavaScript libraries and runtime environments—such as 78 | // browser or node.js. 79 | 80 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 81 | "couch" : false, // Enable globals exposed by CouchDB. 82 | "devel" : false, // Allow development statements e.g. `console.log();`. 83 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 84 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 85 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 86 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 87 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 88 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 89 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 90 | "worker" : false, // Enable globals available when your code is running as a WebWorker. 91 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 92 | "yui" : false, // Enable globals exposed by YUI library. 93 | 94 | // == JSLint Legacy =================================================== 95 | // 96 | // These options are legacy from JSLint. Aside from bug fixes they will 97 | // not be improved in any way and might be removed at any point. 98 | 99 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 100 | "onevar" : false, // Allow only one `var` statement per function. 101 | "passfail" : false, // Stop on first error. 102 | "white" : false, // Check against strict whitespace and indentation rules. 103 | 104 | // == Undocumented Options ============================================ 105 | // 106 | // While I've found these options in [example1][2] and [example2][3] 107 | // they are not described in the [JSHint Options documentation][4]. 108 | // 109 | // [4]: http://www.jshint.com/options/ 110 | 111 | "maxerr" : 100 // Maximum errors before stopping. 112 | } 113 | -------------------------------------------------------------------------------- /lib/schematype.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('buffer').Buffer, 2 | utils = require('./utils'); 3 | 4 | function SchemaType(path, obj, virtual) { 5 | obj = obj || {}; 6 | 7 | this.path = path; 8 | 9 | this.type = obj.type; 10 | 11 | this.enumValues = []; 12 | this.validators = []; 13 | this.getters = []; 14 | this.setters = []; 15 | 16 | this.virtual = !!virtual; 17 | this.autoType = null; 18 | this.isRequired = false; 19 | 20 | // create these stubs in the ctor, so that method calls don't create 21 | // new hidden classes when V8 tries to optimize. 22 | this.minValidator = null; 23 | this.maxValidator = null; 24 | this.enumValidator = null; 25 | this.requiredValidator = null; 26 | 27 | // run helpers based on the object shortcuts 28 | var keys = Object.keys(obj), 29 | key = null; 30 | 31 | for (var i = 0; i < keys.length; ++i) { 32 | key = keys[i]; 33 | if (key === 'type') continue; 34 | 35 | if (typeof this[key] === 'function') { 36 | this[key](obj[key]); 37 | } 38 | } 39 | } 40 | 41 | SchemaType.prototype.cast = function (value) { 42 | if (this.type === String) { 43 | return value + ''; 44 | } 45 | else { 46 | return new (this.type)(value); 47 | } 48 | }; 49 | 50 | SchemaType.prototype.auto = function (autoType) { 51 | this.autoType = autoType; 52 | }; 53 | 54 | SchemaType.prototype.get = function (fn) { 55 | if (typeof fn !== 'function') 56 | throw new TypeError('A getter must be a function.'); 57 | 58 | this.getters.push(fn); 59 | return this; 60 | }; 61 | 62 | SchemaType.prototype.set = function (fn) { 63 | if (typeof fn !== 'function') 64 | throw new TypeError('A setter must be a function.'); 65 | 66 | this.setters.push(fn); 67 | return this; 68 | }; 69 | 70 | SchemaType.prototype.default = function (val) { 71 | if (arguments.length === 1) { 72 | this.defaultValue = typeof val === 'function' ? val : this.cast(val); 73 | } else if (arguments.length > 1) { 74 | this.defaultValue = utils.args(arguments); 75 | } 76 | 77 | return this; 78 | }; 79 | 80 | SchemaType.prototype.validate = function (obj, message) { 81 | if (typeof obj === 'function' || (obj && obj.constructor.name === 'RegExp')) { 82 | if (!message) message = 'Invalid value for schema'; 83 | 84 | this.validators.push([obj, message, 'user defined']); 85 | return this; 86 | } 87 | 88 | // you can also pass multiple validator objects 89 | var i = arguments.length, 90 | arg; 91 | 92 | while (i--) { 93 | arg = arguments[i]; 94 | if (!(arg && arg.constructor.name === 'Object')) { 95 | throw new Error('Invalid validator. Got (' + typeof arg + '): ' + arg); 96 | } 97 | 98 | this.validate(arg.validator, arg.msg || arg.message); 99 | } 100 | 101 | return this; 102 | }; 103 | 104 | SchemaType.prototype.getDefault = function (scope) { 105 | var ret = typeof this.defaultValue === 'function' ? 106 | this.defaultValue.call(scope) : this.defaultValue; 107 | 108 | if (ret !== null && ret !== undefined) { 109 | return this.cast(ret); 110 | } else { 111 | return ret; 112 | } 113 | }; 114 | 115 | //////////////////////////////////////////////// 116 | // Setter shortcuts 117 | //////////////////////////////////////////////// 118 | SchemaType.prototype.lowercase = function () { 119 | return this.set(function (v) { 120 | if (v && v.toLowerCase) return v.toLowerCase(); 121 | return v; 122 | }); 123 | }; 124 | 125 | SchemaType.prototype.uppercase = function () { 126 | return this.set(function (v) { 127 | if (v && v.toUpperCase) return v.toUpperCase(); 128 | return v; 129 | }); 130 | }; 131 | 132 | SchemaType.prototype.trim = function () { 133 | return this.set(function (v) { 134 | if (v && v.trim) return v.trim(); 135 | return v; 136 | }); 137 | }; 138 | 139 | //////////////////////////////////////////////// 140 | // Validator shortcuts 141 | //////////////////////////////////////////////// 142 | SchemaType.prototype.required = function (required, message) { 143 | if (required === false) { 144 | this.validators = this.validators.filter(function (v) { 145 | return v[0] !== this.requiredValidator; 146 | }, this); 147 | 148 | this.isRequired = false; 149 | return this; 150 | } 151 | 152 | var self = this; 153 | this.isRequired = true; 154 | 155 | this.requiredValidator = function (value) { 156 | switch(self.type) { 157 | case Boolean: 158 | return value === true || value === false; 159 | case Date: 160 | return value instanceof Date; 161 | case Number: 162 | return typeof value === 'number' || value instanceof Number; 163 | case String: 164 | return (value instanceof String || typeof value === 'string') && value.length; 165 | case Array: 166 | return !!(value && value.length); 167 | case Buffer: 168 | return !!(value && value.length); 169 | default: 170 | return (value !== undefined) && (value !== null); 171 | } 172 | }; 173 | 174 | if (typeof required === 'string') { 175 | message = required; 176 | required = undefined; 177 | } 178 | 179 | var msg = message || 'is required'; 180 | this.validators.push([this.requiredValidator, msg, 'required']); 181 | 182 | return this; 183 | }; 184 | 185 | SchemaType.prototype.min = function (val, message) { 186 | if (this.minValidator) { 187 | this.validators = this.validators.filter(function (v) { 188 | return v[0] !== this.minValidator; 189 | }, this); 190 | this.minValidator = false; 191 | } 192 | 193 | if (val != null) { //jshint ignore:line 194 | var msg = message || 'must be at or above minimum of: ' + val; 195 | 196 | this.minValidator = function (v) { 197 | return v === null || v >= val; 198 | }; 199 | this.validators.push([this.minValidator, msg, 'min']); 200 | } 201 | 202 | return this; 203 | }; 204 | 205 | SchemaType.prototype.max = function (val, message) { 206 | if (this.maxValidator) { 207 | this.validators = this.validators.filter(function (v) { 208 | return v[0] !== this.maxValidator; 209 | }, this); 210 | this.maxValidator = false; 211 | } 212 | 213 | if (val != null) { //jshint ignore:line 214 | var msg = message || 'must be at or below maximum of: ' + val; 215 | 216 | this.maxValidator = function (v) { 217 | return v === null || v <= val; 218 | }; 219 | this.validators.push([this.maxValidator, msg, 'max']); 220 | } 221 | 222 | return this; 223 | }; 224 | 225 | SchemaType.prototype.match = function (rgx, message) { 226 | var msg = message || 'must match: ' + rgx; 227 | 228 | function matchValidator (v) { 229 | return v !== null && v !== undefined && v !== '' ? rgx.test(v) : true; 230 | } 231 | 232 | this.validators.push([matchValidator, msg, 'regexp']); 233 | return this; 234 | }; 235 | 236 | SchemaType.prototype.enum = function () { 237 | if (this.enumValidator) { 238 | this.validators = this.validators.filter(function (v) { 239 | return v[0] !== this.enumValidator; 240 | }, this); 241 | this.enumValidator = false; 242 | } 243 | 244 | if (arguments[0] === undefined || arguments[0] === false) { 245 | return this; 246 | } 247 | 248 | var values, errorMessage; 249 | 250 | if (utils.isObject(arguments[0])) { 251 | values = arguments[0].values; 252 | errorMessage = arguments[0].message; 253 | } 254 | else if (Array.isArray(arguments[0])) { 255 | values = arguments[0]; 256 | } 257 | else { 258 | values = utils.args(arguments); 259 | } 260 | 261 | if (!errorMessage) { 262 | errorMessage = 'must be within defined enum: [' + values + ']'; 263 | } 264 | 265 | for (var i = 0; i < values.length; ++i) { 266 | if (values[i] !== undefined) { 267 | this.enumValues.push(this.cast(values[i])); 268 | } 269 | } 270 | 271 | var vals = this.enumValues; 272 | this.enumValidator = function (v) { 273 | return v === undefined || vals.indexOf(v) !== -1; 274 | }; 275 | this.validators.push([this.enumValidator, errorMessage, 'enum']); 276 | 277 | return this; 278 | }; 279 | 280 | //////////////////////////////////////////////// 281 | // Apply functions 282 | //////////////////////////////////////////////// 283 | SchemaType.prototype.applyGetters = function (value, scope) { 284 | var v = value; 285 | for (var l = this.getters.length - 1; l >= 0; --l) { 286 | v = this.getters[l].call(scope, v, this); 287 | } 288 | 289 | return v; 290 | }; 291 | 292 | SchemaType.prototype.applySetters = function (value, scope) { 293 | var v = value; 294 | for (var l = this.setters.length - 1; l >= 0; --l) { 295 | v = this.setters[l].call(scope, v, this); 296 | } 297 | 298 | return v; 299 | }; 300 | 301 | SchemaType.prototype.applyValidators = function (value, fn, scope) { 302 | var errorMessage = '', 303 | path = this.path, 304 | count = this.validators.length; 305 | 306 | if (!count) return fn(); 307 | 308 | function _validate(ok, message, type, val) { 309 | if (!ok && ok !== undefined) { 310 | errorMessage += 'Value at path "' + path + '" ' + message + ', got: ' + val + '\n'; 311 | } 312 | 313 | if (--count === 0) { 314 | fn(errorMessage ? new Error(errorMessage) : null); 315 | } 316 | } 317 | 318 | this.validators.forEach(function (v) { 319 | var validator = v[0], 320 | message = v[1], 321 | type = v[2]; 322 | 323 | if (validator instanceof RegExp) { 324 | _validate(validator.test(value), message, type, value); 325 | } 326 | else if (typeof validator === 'function') { 327 | if (validator.length === 2) { 328 | validator.call(scope, value, function (ok) { 329 | _validate(ok, message, type, value); 330 | }); 331 | } 332 | else { 333 | _validate(validator.call(scope, value), message, type, value); 334 | } 335 | } 336 | }); 337 | }; 338 | 339 | module.exports = SchemaType; 340 | -------------------------------------------------------------------------------- /test/unit/model.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | async = require('async'), 3 | kouch = require('../../lib/'), 4 | TestSchema = new kouch.Schema({ 5 | _id: { type: String, key: true, auto: 'uuid' }, 6 | name: String 7 | }), 8 | TestModel, 9 | docs = [ 10 | { _id: 'load1', name: 'test doc 1' }, 11 | { _id: 'load2', name: 'test doc 2' }, 12 | { _id: 'load3', name: 'test doc 3' }, 13 | { _id: 'remove1', name: 'test doc 1' }, 14 | { _id: 'remove2', name: 'test doc 2' }, 15 | { _id: 'remove3', name: 'test doc 3' } 16 | ]; 17 | 18 | // register our DB middleware 19 | require('../fixtures/db'); 20 | 21 | describe('Kouch.Model', function () { 22 | before(function (done) { 23 | TestModel = kouch.model('ModelTest', 'default', TestSchema); 24 | 25 | async.each(docs, function (item, _cb) { 26 | kouch.buckets.default.insert(item._id, item, _cb); 27 | }, done); 28 | }); 29 | 30 | describe('#ctor', function () { 31 | 32 | }); 33 | 34 | describe('#save', function () { 35 | it('Should save the document', function (done) { 36 | var mdl = new TestModel({ name: 'John Smith', notInSchema: 'something' }); 37 | 38 | mdl.save(function (err) { 39 | expect(err).to.not.exist; 40 | 41 | expect(mdl._id).to.be.a('string'); 42 | expect(mdl.name).to.equal('John Smith'); 43 | expect(mdl.notInSchema).to.not.exist; 44 | expect(mdl._doc.notInSchema).to.not.exist; 45 | 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe('#remove', function () { 52 | 53 | }); 54 | 55 | describe('#validate', function () { 56 | 57 | }); 58 | 59 | describe('#getValue', function () { 60 | 61 | }); 62 | 63 | describe('#setValue', function () { 64 | 65 | }); 66 | 67 | describe('#get', function () { 68 | 69 | }); 70 | 71 | describe('#set', function () { 72 | 73 | }); 74 | 75 | describe('#toObject', function () { 76 | it('Should properly serialize the model', function () { 77 | var doc = { _id: 'something', name: 'le test', notInSchema: 'me' }, 78 | result = { _id: 'something', name: 'le test' }, 79 | model = new TestModel(doc); 80 | 81 | expect(model.toObject()).to.eql(result); 82 | }); 83 | }); 84 | 85 | describe('#toJSON', function () { 86 | it('Should properly serialize the model', function () { 87 | var doc = { _id: 'something', name: 'le test', notInSchema: 'me' }, 88 | result = { _id: 'something', name: 'le test' }, 89 | model = new TestModel(doc); 90 | 91 | expect(JSON.stringify(model)).to.equal(JSON.stringify(result)); 92 | }); 93 | }); 94 | 95 | describe('#model', function () { 96 | 97 | }); 98 | 99 | describe('#isModified', function () { 100 | it('Should be modified when created from an object', function () { 101 | var doc = { _id: 'modifiedTest', name: 'modified' }, 102 | model = new TestModel(doc); 103 | 104 | expect(model.isModified()).to.be.true; 105 | }); 106 | 107 | it('Should not be modified when initially loaded', function (done) { 108 | TestModel.load('load1', function (err, model) { 109 | expect(err).to.not.exist; 110 | expect(model.isModified()).to.be.false; 111 | done(err); 112 | }); 113 | }); 114 | 115 | it('Should set properly check the modified state of a single path', function () { 116 | var doc = { _id: 'modifiedTest', name: 'not modified' }, 117 | model = new TestModel(doc); 118 | 119 | expect(model.isModified()).to.be.true; 120 | 121 | model.resetModified('name'); 122 | 123 | expect(model.isModified()).to.be.true; 124 | expect(model.isModified('_id')).to.be.true; 125 | expect(model.isModified('name')).to.be.false; 126 | }); 127 | }); 128 | 129 | describe('#modifiedPaths', function () { 130 | it('Should return all the paths that are modified', function (done) { 131 | TestModel.load('load1', function (err, model) { 132 | expect(err).to.not.exist; 133 | expect(model.modifiedPaths()).to.be.empty; 134 | 135 | model.name = 'CHANGED'; 136 | 137 | expect(model.modifiedPaths()).to.eql(['name']); 138 | done(err); 139 | }); 140 | }); 141 | }); 142 | 143 | describe('#setModified', function () { 144 | it('Should set the path as modified', function () { 145 | var doc = { _id: 'modifiedTest', name: 'not modified' }, 146 | model = new TestModel(doc); 147 | 148 | expect(model.isModified()).to.be.true; 149 | 150 | model.resetModified(); 151 | 152 | expect(model.isModified()).to.be.false; 153 | 154 | model.setModified('_id'); 155 | 156 | expect(model.isModified()).to.be.true; 157 | expect(model.isModified('_id')).to.be.true; 158 | expect(model.isModified('name')).to.be.false; 159 | }); 160 | }); 161 | 162 | describe('#resetModified', function () { 163 | it('Should set reset the modified state of the entire model', function () { 164 | var doc = { _id: 'modifiedTest', name: 'not modified' }, 165 | model = new TestModel(doc); 166 | 167 | expect(model.isModified()).to.be.true; 168 | 169 | model.resetModified(); 170 | 171 | expect(model.isModified()).to.be.false; 172 | }); 173 | 174 | it('Should set reset the modified state of a single path', function () { 175 | var doc = { _id: 'modifiedTest', name: 'not modified' }, 176 | model = new TestModel(doc); 177 | 178 | expect(model.isModified()).to.be.true; 179 | 180 | model.resetModified('name'); 181 | 182 | expect(model.isModified()).to.be.true; 183 | expect(model.isModified('_id')).to.be.true; 184 | expect(model.isModified('name')).to.be.false; 185 | }); 186 | }); 187 | 188 | /////// 189 | // Statics 190 | /////// 191 | 192 | describe('.compile', function () { 193 | 194 | }); 195 | 196 | describe('.load', function () { 197 | it('Should load the proper document', function (done) { 198 | TestModel.load('load1', function (err, model) { 199 | expect(err).to.not.exist; 200 | expect(model).to.be.an.instanceOf(TestModel); 201 | 202 | expect(model._id).to.equal('load1'); 203 | expect(model._doc).to.eql(docs[0]); 204 | 205 | done(); 206 | }); 207 | }); 208 | 209 | it('Should load multiple documents properly', function (done) { 210 | TestModel.load(['load1', 'load2'], function (err, models) { 211 | expect(err).to.not.exist; 212 | 213 | expect(models[0]).to.be.an.instanceOf(TestModel); 214 | expect(models[0]._id).to.equal('load1'); 215 | expect(models[0]._doc).to.eql(docs[0]); 216 | 217 | expect(models[1]).to.be.an.instanceOf(TestModel); 218 | expect(models[1]._id).to.equal('load2'); 219 | expect(models[1]._doc).to.eql(docs[1]); 220 | 221 | done(); 222 | }); 223 | }); 224 | }); 225 | 226 | describe('.remove', function () { 227 | it('Should remove the document properly', function (done) { 228 | TestModel.remove('remove1', function (err) { 229 | expect(err).to.not.exist; 230 | 231 | TestModel.load('remove1', function (err, model) { 232 | expect(err).to.not.exist; 233 | expect(model).to.not.exist; 234 | 235 | done(); 236 | }); 237 | }); 238 | }); 239 | 240 | /*it('Should remove multiple documents properly', function (done) { 241 | TestModel.remove(['remove2', 'remove3'], function (err) { 242 | expect(err).to.not.exist; 243 | 244 | TestModel.load('remove3', function (err) { 245 | expect(err).to.be.an.instanceOf(Error); 246 | expect(err.message).to.contain('key not found'); 247 | 248 | done(); 249 | }); 250 | }); 251 | });*/ 252 | }); 253 | 254 | describe('.insert', function () { 255 | it('Should insert a document properly', function (done) { 256 | var doc = { _id: 'insert1', name: 'herp' }; 257 | 258 | TestModel.insert(doc, function (err, model) { 259 | expect(err).to.not.exist; 260 | 261 | expect(model).to.be.an.instanceOf(TestModel); 262 | expect(model._id).to.equal('insert1'); 263 | expect(model._doc).to.eql(doc); 264 | 265 | TestModel.load('insert1', function (err, _model) { 266 | expect(err).to.not.exist; 267 | 268 | expect(_model).to.be.an.instanceOf(TestModel); 269 | expect(_model._id).to.equal('insert1'); 270 | expect(_model._doc).to.eql(doc); 271 | 272 | done(); 273 | }); 274 | }); 275 | }); 276 | 277 | /*it('Should insert multiple documents properly', function (done) { 278 | var _docs = [ 279 | { _id: 'multiInsert1', name: 'Multi Insert 1' }, 280 | { _id: 'multiInsert2', name: 'Multi Insert 2' }, 281 | { _id: 'multiInsert3', name: 'Multi Insert 3' }, 282 | { _id: 'multiInsert4', name: 'Multi Insert 4' } 283 | ]; 284 | 285 | TestModel.insert(_docs, function (err, models) { 286 | expect(err).to.not.exist; 287 | 288 | expect(models[0]).to.be.an.instanceOf(TestModel); 289 | expect(models[0]._id).to.equal('multiInsert1'); 290 | expect(models[0]._doc).to.eql(_docs[0]); 291 | 292 | expect(models[1]).to.be.an.instanceOf(TestModel); 293 | expect(models[1]._id).to.equal('multiInsert2'); 294 | expect(models[1]._doc).to.eql(_docs[1]); 295 | 296 | expect(models[2]).to.be.an.instanceOf(TestModel); 297 | expect(models[2]._id).to.equal('multiInsert3'); 298 | expect(models[2]._doc).to.eql(_docs[2]); 299 | 300 | TestModel.load(['multiInsert1', 'multiInsert3'], function (err, _model) { 301 | expect(err).to.not.exist; 302 | 303 | expect(models[0]).to.be.an.instanceOf(TestModel); 304 | expect(models[0]._id).to.equal('multiInsert1'); 305 | expect(models[0]._doc).to.eql(_docs[0]); 306 | 307 | expect(models[2]).to.be.an.instanceOf(TestModel); 308 | expect(models[2]._id).to.equal('multiInsert3'); 309 | expect(models[2]._doc).to.eql(_docs[2]); 310 | 311 | expect(_model).to.be.ok; 312 | 313 | done(); 314 | }); 315 | }); 316 | });*/ 317 | }); 318 | 319 | describe('.key', function () { 320 | 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | events = require('events'), 3 | utils = require('./utils'), 4 | SchemaType = require('./schematype'); 5 | 6 | function Schema(obj, options) { 7 | if (!(this instanceof Schema)) 8 | return new Schema(obj, options); 9 | 10 | events.EventEmitter.call(this); 11 | 12 | this._cache = {}; 13 | 14 | /** 15 | * Schema as flat paths. 16 | * 17 | * ####Example: 18 | * { 19 | * '_id' : SchemaType, 20 | * 'nested.key': SchemaType 21 | * } 22 | * 23 | * @property paths 24 | * @private 25 | */ 26 | this.paths = {}; 27 | 28 | this.virtuals = {}; 29 | this.nested = {}; 30 | this.methods = {}; 31 | this.statics = {}; 32 | 33 | /** 34 | * Schema as a tree. 35 | * 36 | * ####Example: 37 | * { 38 | * '_id' : ObjectId, 39 | * 'nested' : { 40 | * 'key' : String 41 | * } 42 | * } 43 | * 44 | * @property tree 45 | * @private 46 | */ 47 | this.tree = {}; 48 | 49 | this.callQueue = []; 50 | 51 | this.options = this.defaultOptions(options); 52 | 53 | this.key = { 54 | path: null, 55 | prefix: false, 56 | suffix: false 57 | }; 58 | 59 | // setup the schema key 60 | this._setupKey(obj); 61 | 62 | // build the schema paths 63 | if (obj) { 64 | this.add(obj); 65 | } 66 | } 67 | 68 | util.inherits(Schema, events.EventEmitter); 69 | 70 | Schema.prototype._setupKey = function (obj) { 71 | var found = false; 72 | 73 | for (var k in obj) { 74 | if (obj[k] && obj[k].key) { 75 | found = true; 76 | 77 | this.key.path = k; 78 | this.key.prefix = obj[k].prefix; 79 | this.key.suffix = obj[k].suffix; 80 | 81 | if (obj[k].type !== String && obj[k].type !== Number) { 82 | throw new TypeError('Schema key must be a String or Number'); 83 | } 84 | 85 | break; 86 | } 87 | } 88 | 89 | // if we didn't find a key, add a descriptor for default value 90 | if (!found) { 91 | this.key.path = '_id'; 92 | this.add({ 93 | _id: { type: String, auto: 'uuid', key: true } 94 | }); 95 | } 96 | }; 97 | 98 | /** 99 | * Returns default options for this schema, merged with `options`. 100 | * 101 | * @method defaultOptions 102 | * @private 103 | * @param {Object} options 104 | * @return {Object} The new options extended with defaults. 105 | */ 106 | Schema.prototype.defaultOptions = function (options) { 107 | return utils.options(options, { 108 | }); 109 | }; 110 | 111 | /** 112 | * Adds key path / schema type pairs to this schema. 113 | * 114 | * ####Example: 115 | * 116 | * var ToySchema = new Schema; 117 | * ToySchema.add({ name: String, color: Number, price: Number }); 118 | * 119 | * @method add 120 | * @param obj {Object} The object descriptor. 121 | * @param prefix {String} The path prefix. 122 | */ 123 | Schema.prototype.add = function (obj, prefix) { 124 | prefix = prefix || ''; 125 | 126 | var keys = Object.keys(obj); 127 | 128 | for (var i = 0; i < keys.length; ++i) { 129 | var key = keys[i]; 130 | 131 | if (!obj[key]) { 132 | throw new TypeError('Invalid value for schema path `' + prefix + key + '`'); 133 | } 134 | 135 | // if object, and is normal object, and has isn't a descriptor type 136 | if (utils.isObject(obj[key]) && 137 | (!obj[key].constructor || obj[key].constructor.name === 'Object') && 138 | (!obj[key].type || obj[key].type.type) 139 | ) { 140 | // nested object path, recurse 141 | if (Object.keys(obj[key]).length) { 142 | this.nested[prefix + key] = true; 143 | this.add(obj[key], prefix + key + '.'); 144 | } 145 | // mixed type descriptor 146 | else { 147 | this.path(prefix + key, obj[key]); 148 | } 149 | } 150 | // mixed type descriptor 151 | else { 152 | this.path(prefix + key, obj[key]); 153 | } 154 | } 155 | }; 156 | 157 | /** 158 | * Gets/sets schema paths. 159 | * 160 | * Sets a path (if arity 2) 161 | * Gets a path (if arity 1) 162 | * 163 | * ####Example 164 | * 165 | * schema.path('name') // returns a SchemaType 166 | * schema.path('name', Number) // changes the schemaType of `name` to Number 167 | * 168 | * @method path 169 | * @param path {String} The path to get/set. 170 | * @param constructor {Object} The ctor this path uses. 171 | */ 172 | Schema.prototype.path = function (path, obj) { 173 | if (!obj) { 174 | return this.paths[path]; 175 | } 176 | 177 | // some path names conflict with document methods 178 | if (Schema.reserved[path]) { 179 | throw new Error('`' + path + '` may not be used as a schema pathname'); 180 | } 181 | 182 | // update the tree 183 | var subpaths = path.split('.'), 184 | last = subpaths.pop(), 185 | branch = this.tree; 186 | 187 | subpaths.forEach(function(sub, i) { 188 | if (!branch[sub]) branch[sub] = {}; 189 | 190 | if (typeof branch[sub] !== 'object') { 191 | var msg = 'Cannot set nested path `' + path + '`. ' + 192 | 'Parent path `' + 193 | subpaths.slice(0, i).concat([sub]).join('.') + 194 | '` already set to type ' + branch[sub].name + '.'; 195 | 196 | throw new Error(msg); 197 | } 198 | 199 | branch = branch[sub]; 200 | }); 201 | 202 | branch[last] = utils.clone(obj); 203 | 204 | this.paths[path] = Schema.interpretAsType(path, obj); 205 | return this; 206 | }; 207 | 208 | Schema.prototype.virtualpath = function (name) { 209 | return this.virtuals[name]; 210 | }; 211 | 212 | /** 213 | * Returns an Array of path strings that are required by this schema. 214 | * 215 | * @method requiredPaths 216 | * @return {Array} An array of the required paths 217 | */ 218 | Schema.prototype.requiredPaths = function requiredPaths () { 219 | if (this._cache.requiredpaths) 220 | return this._cache.requiredpaths; 221 | 222 | var paths = Object.keys(this.paths), 223 | i = paths.length, 224 | ret = []; 225 | 226 | while (i--) { 227 | var path = paths[i]; 228 | if (this.paths[path].isRequired) ret.push(path); 229 | } 230 | 231 | this._cache.requiredpaths = ret; 232 | 233 | return ret; 234 | }; 235 | 236 | /** 237 | * Returns the pathType of `path` for this schema. 238 | * 239 | * Given a path, returns whether it is a real, virtual, nested, or ad-hoc/undefined path. 240 | * 241 | * @method pathType 242 | * @param path {String} 243 | * @return {String} 244 | */ 245 | Schema.prototype.pathType = function (path) { 246 | if (path in this.paths) return 'real'; 247 | if (path in this.virtuals) return 'virtual'; 248 | if (path in this.nested) return 'nested'; 249 | 250 | return 'adhocOrUndefined'; 251 | }; 252 | 253 | /** 254 | * Adds a method call to the queue. 255 | * 256 | * @param name {String} name of the document method to call later 257 | * @param args {Array} arguments to pass to the method 258 | * @api private 259 | */ 260 | Schema.prototype.queue = function (name, args) { 261 | this.callQueue.push([name, args]); 262 | return this; 263 | }; 264 | 265 | /** 266 | * Defines a pre hook for the document. 267 | * 268 | * ####Example 269 | * 270 | * var toySchema = new Schema(..); 271 | * 272 | * toySchema.pre('save', function (next) { 273 | * if (!this.created) this.created = new Date; 274 | * next(); 275 | * }) 276 | * 277 | * toySchema.pre('validate', function (next) { 278 | * if (this.name != 'Woody') this.name = 'Woody'; 279 | * next(); 280 | * }) 281 | * 282 | * @param {String} method 283 | * @param {Function} callback 284 | * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3 285 | * @api public 286 | */ 287 | Schema.prototype.pre = function () { 288 | return this.queue('pre', arguments); 289 | }; 290 | 291 | /** 292 | * Defines a post hook for the document 293 | * 294 | * Post hooks fire `on` the event emitted from document instances of Models compiled from this schema. 295 | * 296 | * var schema = new Schema(..); 297 | * schema.post('save', function (doc) { 298 | * console.log('this fired after a document was saved'); 299 | * }); 300 | * 301 | * var Model = kouch.model('Model', schema); 302 | * 303 | * var m = new Model(..); 304 | * m.save(function (err) { 305 | * console.log('this fires after the `post` hook'); 306 | * }); 307 | * 308 | * @param {String} method name of the method to hook 309 | * @param {Function} fn callback 310 | * @see hooks.js https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3 311 | * @api public 312 | */ 313 | Schema.prototype.post = function () { 314 | return this.queue('on', arguments); 315 | }; 316 | 317 | /** 318 | * Adds an instance method to documents constructed from Models compiled from this schema. 319 | * 320 | * ####Example 321 | * 322 | * var schema = kittySchema = new Schema(..); 323 | * 324 | * schema.method('meow', function () { 325 | * console.log('meeeeeoooooooooooow'); 326 | * }) 327 | * 328 | * var Kitty = kouch.model('Kitty', schema); 329 | * 330 | * var fizz = new Kitty(); 331 | * fizz.meow(); // meeeeeooooooooooooow 332 | * 333 | * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as methods. 334 | * 335 | * schema.method({ 336 | * purr: function () {}, 337 | * scratch: function () {} 338 | * }); 339 | * 340 | * // later 341 | * fizz.purr(); 342 | * fizz.scratch(); 343 | * 344 | * @method method 345 | * @param name {String|Object} 346 | * @param [fn] {Function} 347 | */ 348 | Schema.prototype.method = function (name, fn) { 349 | if (typeof name !== 'string') { 350 | for (var i in name) 351 | this.methods[i] = name[i]; 352 | } else { 353 | this.methods[name] = fn; 354 | } 355 | 356 | return this; 357 | }; 358 | 359 | /** 360 | * Adds static 'class' methods to Models compiled from this schema. 361 | * 362 | * ####Example 363 | * 364 | * var schema = new Schema(..); 365 | * schema.static('findByName', function (name, callback) { 366 | * return this.find({ name: name }, callback); 367 | * }); 368 | * 369 | * var Drink = kouch.model('Drink', schema); 370 | * Drink.findByName('sanpellegrino', function (err, drinks) { 371 | * // 372 | * }); 373 | * 374 | * If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics. 375 | * 376 | * @method static 377 | * @param name {String|Object} 378 | * @param [fn] {Function} 379 | */ 380 | Schema.prototype.static = function(name, fn) { 381 | if ('string' !== typeof name) { 382 | for (var i in name) 383 | this.statics[i] = name[i]; 384 | } else { 385 | this.statics[name] = fn; 386 | } 387 | 388 | return this; 389 | }; 390 | 391 | /** 392 | * Sets/gets a schema option. 393 | * 394 | * @method set 395 | * @param key, {String} option name 396 | * @param [value] {Object} if not passed, the current option value is returned 397 | */ 398 | Schema.prototype.set = function (key, value) { 399 | if (1 === arguments.length) { 400 | return this.options[key]; 401 | } 402 | 403 | this.options[key] = value; 404 | 405 | return this; 406 | }; 407 | 408 | Schema.prototype.get = Schema.prototype.set; 409 | 410 | /** 411 | * Creates a virtual type with the given name. 412 | * 413 | * @param name {String} 414 | * @param [options] {Object} 415 | * @return {VirtualType} 416 | */ 417 | Schema.prototype.virtual = function (name, options) { 418 | var parts = name.split('.'); 419 | 420 | this.virtuals[name] = parts.reduce(function (mem, part, i) { 421 | if (!mem[part]) { 422 | mem[part] = (i === parts.length-1) ? new SchemaType(name, options, true) : {}; 423 | } 424 | 425 | return mem[part]; 426 | }, this.tree); 427 | 428 | return this.virtuals[name]; 429 | }; 430 | 431 | /** 432 | * Reserved document keys. 433 | * 434 | * Keys in this object are names that are rejected in schema declarations b/c they conflict with kouch functionality. 435 | * Using these key name will throw an error. 436 | * 437 | * _NOTE:_ Use of these terms as method names is permitted, but play at your own risk, as they may be existing 438 | * document methods you are stomping on. 439 | * 440 | * var schema = new Schema(..); 441 | * schema.methods.init = function () {} // potentially breaking 442 | * 443 | * @property reserved 444 | * @static 445 | */ 446 | Schema.reserved = { 447 | on: 1, 448 | db: 1, 449 | set: 1, 450 | get: 1, 451 | init: 1, 452 | isNew: 1, 453 | errors: 1, 454 | schema: 1, 455 | options: 1, 456 | modelName: 1, 457 | collection: 1, 458 | toObject: 1, 459 | emit: 1, // EventEmitter 460 | _events: 1, // EventEmitter 461 | _pres: 1 462 | }; 463 | 464 | /** 465 | * Converts type arguments into Kouch Types. 466 | * 467 | * @param {String} path 468 | * @param {Object} obj constructor 469 | * @api private 470 | */ 471 | 472 | Schema.interpretAsType = function (path, obj) { 473 | if (obj.constructor && obj.constructor.name !== 'Object') 474 | obj = { type: obj }; 475 | 476 | return new SchemaType(path, obj); 477 | }; 478 | 479 | module.exports = Schema; 480 | -------------------------------------------------------------------------------- /lib/model.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | uuid = require('uuid'), 3 | hooks = require('hooks'), 4 | events = require('events'), 5 | utils = require('./utils'); 6 | 7 | var _noop = function () {}; 8 | 9 | function Model(doc) { 10 | if (!(this instanceof Model)) 11 | return new Model(doc); 12 | 13 | events.EventEmitter.call(this); 14 | 15 | this._doc = buildDefaultDocument.call(this); 16 | 17 | this._cache = {}; 18 | 19 | if (doc) { 20 | this.set(doc, undefined, true); 21 | } 22 | 23 | 24 | this._registerHooks(); 25 | } 26 | 27 | util.inherits(Model, events.EventEmitter); 28 | 29 | /** 30 | * Set up middleware support 31 | */ 32 | for (var k in hooks) { 33 | Model.prototype[k] = Model[k] = hooks[k]; 34 | } 35 | 36 | /** 37 | * Load a model from the associated bucket 38 | * 39 | * TODO: Combine .load and .loadMulti into a single function that handles both cases. 40 | * 41 | * @method load 42 | * @static 43 | * @param key {String|Array} The key(s) for the document(s). 44 | * @param [options] {Object} The options for the get operation. 45 | * @param [callback] {Function} The function to call when the get returns. 46 | */ 47 | Model.load = function (key, options, cb) { 48 | if (typeof options === 'function') { 49 | cb = options; 50 | options = null; 51 | } 52 | 53 | cb = cb || _noop; 54 | options = options || {}; 55 | 56 | if (Array.isArray(key)) { 57 | return Model.loadMulti.call(this, key, cb); 58 | } 59 | else if (!key) { 60 | var error = new Error('Key is required to load a document.'); 61 | 62 | if (cb) { 63 | return cb(error); 64 | } 65 | else { 66 | throw error; 67 | } 68 | } 69 | 70 | var self = this; 71 | this.bucket.get(this.key(key), options, function (err, result) { 72 | if (err) { 73 | if (err.message === 'key not found') { 74 | return cb(); 75 | } 76 | else { 77 | return cb(err); 78 | } 79 | } 80 | 81 | var SubModel = self.model(self.modelName), 82 | mdl = new SubModel(result.value); 83 | 84 | mdl._cache.cas = result.cas; 85 | mdl.resetModified(); 86 | 87 | cb(null, mdl); 88 | }); 89 | 90 | return this; 91 | }; 92 | 93 | /** 94 | * Load multiple models from the associated bucket 95 | * 96 | * @method loadMulti 97 | * @static 98 | * @param keys {Array} The keys for the documents. 99 | * @param [callback] {Function} The function to call when the get returns. 100 | */ 101 | Model.loadMulti = function (keys, cb) { 102 | if (typeof options === 'function') { 103 | cb = options; 104 | options = null; 105 | } 106 | 107 | cb = cb || _noop; 108 | 109 | var self = this; 110 | this.bucket.getMulti(keys.map(this.key.bind(this)), function (err, results) { 111 | if (err) return cb(err); 112 | 113 | var SubModel = self.model(self.modelName), 114 | models = [], 115 | mdl; 116 | 117 | for (var k in results) { 118 | mdl = new SubModel(results[k].value); 119 | mdl._cache.cas = results[k].cas; 120 | mdl.resetModified(); 121 | 122 | models.push(mdl); 123 | } 124 | 125 | cb(null, models); 126 | }); 127 | 128 | return this; 129 | }; 130 | 131 | /** 132 | * Remove a model from the associated bucket 133 | * 134 | * TODO: Combine .remove and .removeMulti into a single function that handles both cases. 135 | * 136 | * @method remove 137 | * @static 138 | * @param key {String|Array} The key(s) for the document(s). 139 | * @param [options] {Object} The options for the remove operation. 140 | * @param [callback] {Function} The function to call when the remove returns. 141 | */ 142 | Model.remove = function (key, options, cb) { 143 | if (typeof options === 'function') { 144 | cb = options; 145 | options = null; 146 | } 147 | 148 | cb = cb || _noop; 149 | options = options || {}; 150 | 151 | if (Array.isArray(key)) { 152 | return Model.removeMulti.call(this, key, cb); 153 | } 154 | else if (!key) { 155 | var error = new Error('Key is required to remove a document.'); 156 | 157 | if (cb) { 158 | return cb(error); 159 | } 160 | else { 161 | throw error; 162 | } 163 | } 164 | 165 | this.bucket.remove(this.key(key), options, function (err, result) { 166 | if (err) { 167 | if (err.message === 'key not found') { 168 | return cb(); 169 | } 170 | else { 171 | return cb(err); 172 | } 173 | } 174 | 175 | cb(null, result); 176 | }); 177 | 178 | return this; 179 | }; 180 | 181 | /** 182 | * Remove multiple models from the associated bucket 183 | * 184 | * @method removeMulti 185 | * @static 186 | * @param keys {Array} The keys for the documents. 187 | * @param [callback] {Function} The function to call when the remove returns. 188 | */ 189 | Model.removeMulti = function (keys, cb) { 190 | throw new Error('Not Yet Implemented!'); 191 | // if (typeof options === 'function') { 192 | // cb = options; 193 | // options = null; 194 | // } 195 | 196 | // cb = cb || _noop; 197 | 198 | // this.bucket.removeMulti(keys.map(this.key.bind(this)), options, function (err, results) { 199 | // if (err) return cb(err); 200 | 201 | // cb(null, results); 202 | // }); 203 | 204 | // return this; 205 | }; 206 | 207 | /** 208 | * Create a new document and insert it. 209 | * 210 | * @method insert 211 | * @static 212 | * @param doc {Object|Array} The document(s) to insert. 213 | * @param [options] {Object} The options for the insert operation. 214 | * @param [callback] {Function} The function to call upon the completion of the operation. 215 | */ 216 | Model.insert = function (doc, options, cb) { 217 | if (typeof options === 'function') { 218 | cb = options; 219 | options = null; 220 | } 221 | 222 | cb = cb || _noop; 223 | options = options || {}; 224 | 225 | if (Array.isArray(doc)) { 226 | return Model.insertMulti.call(this, key, cb); 227 | } 228 | 229 | var SubModel = this.model(this.modelName), 230 | data = new SubModel(doc); 231 | 232 | this.bucket.insert(data.key(), data.toObject(), options, function (err, result) { 233 | cb(err, data); 234 | }); 235 | }; 236 | 237 | /** 238 | * Create many new documents at once and insert them all. 239 | * 240 | * @method insertMulti 241 | * @static 242 | * @param doc {Array} The documents to insert. 243 | * @param [callback] {Function} The function to call upon the completion of the operation. 244 | */ 245 | Model.insertMulti = function (docs, cb) { 246 | throw new Error('Not Yet Implemented!'); 247 | // if (typeof options === 'function') { 248 | // cb = options; 249 | // options = null; 250 | // } 251 | 252 | // cb = cb || _noop; 253 | 254 | // var SubModel = this.model(this.modelName), 255 | // models = []; 256 | 257 | // docs = docs.reduce(function (obj, doc) { 258 | // var data = new SubModel(doc); 259 | 260 | // models.push(data); 261 | 262 | // obj[data.key()] = { value: data.toObject() }; 263 | 264 | // return obj; 265 | // }, {}); 266 | 267 | // this.bucket.insertMulti(docs, options, function (err) { 268 | // cb(err, models); 269 | // }); 270 | }; 271 | 272 | // Model.upsert = function (key, value, options, cb) { 273 | // }; 274 | 275 | // Model.upsertMulti = function (kvPairs, options, cb) { 276 | // }; 277 | 278 | // Model.replace = function (key, value, options, cb) { 279 | // }; 280 | 281 | // Model.replaceMulti = function (kvPairs, options, cb) { 282 | // }; 283 | 284 | // Model.append = function (key, str, options, cb) { 285 | // }; 286 | 287 | // Model.appendMulti = function (kvPairs, options, cb) { 288 | // }; 289 | 290 | // Model.prepend = function (key, str, options, cb) { 291 | // }; 292 | 293 | // Model.prependMulti = function (kvPairs, options, cb) { 294 | // }; 295 | 296 | // Model.touch = function (key, options, cb) { 297 | // }; 298 | 299 | // Model.touchMulti = function (kvPairs, options, cb) { 300 | // }; 301 | 302 | // Model.counter = function (key, options, cb) { 303 | // }; 304 | 305 | // Model.counterMulti = function (kvPairs, options, cb) { 306 | // }; 307 | 308 | // Model.query = function (query, cb) { 309 | // }; 310 | 311 | // Model.lock = function (key, options, cb) { 312 | // }; 313 | 314 | // Model.lockMulti = function (keys, options, cb) { 315 | // }; 316 | 317 | // Model.unlock = function (key, options, cb) { 318 | // }; 319 | 320 | // Model.unlockMulti = function (keys, options, cb) { 321 | // }; 322 | 323 | Model.key = Model.prototype.key = function (key) { 324 | key = key || this.get(this.schema.key.path); 325 | 326 | return (this.schema.key.prefix ? this.schema.key.prefix : '') + 327 | key + 328 | (this.schema.key.suffix ? this.schema.key.suffix : ''); 329 | }; 330 | 331 | /** 332 | * Save this model to the associated bucket 333 | * 334 | * @method save 335 | * @param [options] {Object} The options for the save operation. 336 | * @param [callback] {Function} The function to call upon the completion of the operation. 337 | */ 338 | Model.prototype.save = function (options, cb) { 339 | if (typeof options === 'function') { 340 | cb = options; 341 | options = null; 342 | } 343 | 344 | options = options || {}; 345 | cb = cb || _noop; 346 | 347 | if (!this.get(this.schema.key.path)) { 348 | return setImmediate(function () { 349 | cb(new Error('This document has no key value, set one before calling save or use `auto:true`')); 350 | }); 351 | } 352 | 353 | options.cas = this._cache.cas; 354 | 355 | var self = this; 356 | this.bucket.upsert(this.key(), this.toObject(), options, function (err, result) { 357 | if (err) return cb(err); 358 | 359 | self._cache.cas = result.cas; 360 | self.resetModified(); 361 | 362 | cb(null, self); 363 | }); 364 | 365 | return this; 366 | }; 367 | 368 | /** 369 | * Remove this model from the associated bucket 370 | * 371 | * @method remove 372 | * @param [options] {Object} The options for the remove operation. 373 | * @param [callback] {Function} The function to call upon the completion of the operation. 374 | */ 375 | Model.prototype.remove = function (options, cb) { 376 | if (typeof options === 'function') { 377 | cb = options; 378 | options = null; 379 | } 380 | 381 | options = options || {}; 382 | cb = cb || _noop; 383 | 384 | if (!this.get(this.schema.key.path)) { 385 | return setImmediate(function () { 386 | cb(new Error('This document has no key value, set one before calling remove or use `auto:true`')); 387 | }); 388 | } 389 | 390 | options.cas = this._cache.cas; 391 | 392 | var self = this; 393 | this.bucket.remove(this.key(), options, function (err, result) { 394 | if (err) return cb(err); 395 | 396 | self._cache.cas = result.cas; 397 | 398 | cb(); 399 | }); 400 | 401 | return this; 402 | }; 403 | 404 | Model.prototype.validate = function (cb) { 405 | var self = this, 406 | paths = Object.keys(this.schema.paths), 407 | error = null, 408 | validating = null, 409 | total = 0; 410 | 411 | function complete() { 412 | self.emit('validate', self); 413 | cb(error); 414 | } 415 | 416 | if (paths.length === 0) { 417 | complete(); 418 | return this; 419 | } 420 | 421 | validating = {}; 422 | 423 | function validatePath(path) { 424 | if (validating[path]) return; 425 | 426 | validating[path] = true; 427 | total++; 428 | 429 | setImmediate(function () { 430 | var p = self.schema.path(path); 431 | if (!p) return --total || complete(); 432 | 433 | var val = self.getValue(path); 434 | p.applyValidators(val, function (err) { 435 | if (err) { 436 | error = err; 437 | } 438 | 439 | --total || complete(); 440 | }, self); 441 | }); 442 | } 443 | 444 | paths.forEach(validatePath); 445 | 446 | return this; 447 | }; 448 | 449 | /** 450 | * Gets a raw value from a path (no getters) 451 | * 452 | * @param {String} path 453 | * @api private 454 | */ 455 | Model.prototype.getValue = function (path) { 456 | return utils.getValue(path, this._doc); 457 | }; 458 | 459 | /** 460 | * Sets a raw value for a path (no casting, setters, transformations) 461 | * 462 | * @param {String} path 463 | * @param {Object} value 464 | * @api private 465 | */ 466 | Model.prototype.setValue = function (path, val) { 467 | utils.setValue(path, this._doc, val); 468 | return this; 469 | }; 470 | 471 | Model.prototype.get = function (path, skipGetters) { 472 | var type = this.schema.path(path) || this.schema.virtualpath(path), 473 | pieces = path.split('.'), 474 | obj = this._doc; 475 | 476 | // Walk the document object to get the value 477 | for (var i = 0, il = pieces.length; i < il; ++i) { 478 | obj = obj === undefined || obj === null ? undefined : obj[pieces[i]]; 479 | } 480 | 481 | // If there is no value yet, and it should be auto 482 | if (obj === undefined && type && type.autoType) { 483 | if (!this._cache.auto) { 484 | this._cache.auto = {}; 485 | } 486 | 487 | if (!this._cache.auto[path]) { 488 | // custom auto function 489 | if (typeof type.autoType === 'function') { 490 | this.set(path, obj = type.autoType.call(this), true); 491 | } 492 | // for now assume uuid, but maybe support other 'auto' types later 493 | else { 494 | this.set(path, obj = uuid.v4(), true); 495 | } 496 | 497 | this._cache.auto[path] = true; 498 | } 499 | } 500 | 501 | // If there is a schema type, then call the getters 502 | if (type && !skipGetters) { 503 | obj = type.applyGetters(obj, this); 504 | } 505 | 506 | return obj; 507 | }; 508 | 509 | Model.prototype.set = function (path, val, type) { 510 | var constructing = (type === true); 511 | 512 | // when an object is passed: `new Model({ key: val })` 513 | if (typeof path !== 'string') { 514 | if (path === null || path === undefined) { 515 | var _ = path; 516 | path = val; 517 | val = _; 518 | } else { 519 | var prefix = val ? val + '.' : ''; 520 | 521 | if (path instanceof Model) path = path._doc; 522 | 523 | var keys = Object.keys(path), 524 | i = keys.length, 525 | pathtype, 526 | key; 527 | 528 | while (i--) { 529 | key = keys[i]; 530 | pathtype = this.schema.pathType(prefix + key); 531 | 532 | if (path[key] !== null && 533 | path[key] !== undefined && 534 | utils.isObject(path[key]) && 535 | (!path[key].constructor || path[key].constructor.name === 'Object') && 536 | pathtype !== 'virtual' && 537 | !(this.schema.paths[key] && this.schema.paths[key].options.ref) 538 | ) { 539 | this.set(path[key], prefix + key, constructing); 540 | } else if (pathtype === 'real' || pathtype === 'virtual') { 541 | this.set(prefix + key, path[key], constructing); 542 | } else if (path[key] !== undefined) { 543 | this.set(prefix + key, path[key], constructing); 544 | } 545 | } 546 | 547 | return this; 548 | } 549 | } 550 | 551 | var pathType = this.schema.pathType(path); 552 | if (pathType === 'nested'&& 553 | val && 554 | utils.isObject(val) && 555 | (!val.constructor || val.constructor.name === 'Object') 556 | ) { 557 | this.set(val, path, constructing); 558 | return this; 559 | } 560 | 561 | var parts = path.split('.'), 562 | schema; 563 | 564 | if (pathType === 'virtual') { 565 | schema = this.schema.virtualpath(path); 566 | schema.applySetters(val, this); 567 | return this; 568 | } else { 569 | schema = this.schema.path(path); 570 | } 571 | 572 | var priorVal = constructing ? undefined : this.get(path); 573 | 574 | if (schema && val !== undefined) { 575 | val = schema.applySetters(val, this, false, priorVal); 576 | } 577 | 578 | if (schema) { 579 | this._set(path, parts, schema, val); 580 | } 581 | 582 | return this; 583 | }; 584 | 585 | /** 586 | * @method toObject 587 | * @param [options] {Object} 588 | * @param [options.minimize=true] {Boolean} removing undefined values and empty objects 589 | * @param [options.getters=true] {Boolean} run getters when getting values 590 | * @param [options.virtuals] {Boolean} include virtual values in the result 591 | * @param [options.transform] {Function} custom transform function to run 592 | * @return {Object} The resulting object 593 | */ 594 | Model.prototype.toObject = function (options) { 595 | options = options || {}; 596 | options.minimize = options.minimize !== undefined ? options.minimize : true; 597 | options.getters = options.getters !== undefined ? options.getters : true; 598 | 599 | var ret = utils.clone(this._doc, options); 600 | 601 | if (options.virtuals) { 602 | applyGetters(this, ret, 'virtuals', options); 603 | } 604 | 605 | if (options.getters) { 606 | applyGetters(this, ret, 'paths', options); 607 | 608 | // applyGetters for paths will add nested empty objects; 609 | // if minimize is set, we need to remove them. 610 | if (options.minimize) { 611 | ret = minimize(ret) || {}; 612 | } 613 | } 614 | 615 | if (typeof options.transform === 'function') { 616 | var xformed = options.transform(this, ret, options); 617 | 618 | if (typeof xformed !== 'undefined') { 619 | ret = xformed; 620 | } 621 | } 622 | 623 | return ret; 624 | }; 625 | 626 | Model.prototype.toJSON = function (options) { 627 | options = options || {}; 628 | options.json = true; 629 | 630 | return this.toObject(options); 631 | }; 632 | 633 | Model.prototype.model = function (name) { 634 | return this.base.model(name, this.bucketName); 635 | }; 636 | 637 | Model.prototype.isModified = function (path) { 638 | return path ? this._cache.modifiedPaths.indexOf(path) !== -1 639 | : this._cache.modifiedPaths.length !== 0; 640 | }; 641 | 642 | Model.prototype.modifiedPaths = function () { 643 | return this._cache.modifiedPaths; 644 | }; 645 | 646 | Model.prototype.setModified = function (path) { 647 | // lazy create the set of paths 648 | if (!this._cache.modifiedPaths) { 649 | this._cache.modifiedPaths = []; 650 | } 651 | 652 | // if the path isn't already modified, add it to the set 653 | if (this._cache.modifiedPaths.indexOf(path) === -1) { 654 | this._cache.modifiedPaths.push(path); 655 | } 656 | }; 657 | 658 | Model.prototype.resetModified = function (path) { 659 | // reset a single path 660 | if (path) { 661 | var idx = this._cache.modifiedPaths.indexOf(path); 662 | 663 | if (idx !== -1) { 664 | this._cache.modifiedPaths.splice(idx, 1); 665 | } 666 | } 667 | // reset the modified state of the entire model 668 | else { 669 | this._cache.modifiedPaths.length = 0; 670 | } 671 | }; 672 | 673 | Model.prototype._set = function (path, parts, schema, val) { 674 | var obj = this._doc, 675 | i = 0, 676 | l = parts.length; 677 | 678 | this.setModified(path); 679 | 680 | for (; i < l; ++i) { 681 | var next = i + 1, 682 | last = (next === l); 683 | 684 | if (last) { 685 | obj[parts[i]] = val; 686 | } else { 687 | if (obj[parts[i]] && obj[parts[i]].constructor.name === 'Object') { 688 | obj = obj[parts[i]]; 689 | } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { 690 | obj = obj[parts[i]]; 691 | } else { 692 | obj = obj[parts[i]] = {}; 693 | } 694 | } 695 | } 696 | }; 697 | 698 | Model.prototype._processQueue = function () { 699 | var q = this.schema && this.schema.callQueue; 700 | if (q) { 701 | for (var i = 0, il = q.length; i < il; ++i) { 702 | this[q[i][0]].apply(this, q[i][1]); 703 | } 704 | } 705 | 706 | return this; 707 | }; 708 | 709 | Model.prototype._registerHooks = function () { 710 | this.pre('save', function validation(next) { 711 | return this.validate(next); 712 | }); 713 | 714 | this._processQueue(); 715 | }; 716 | 717 | Model.compile = function (name, bucketName, bucket, schema, base) { 718 | // create a class specifically for this schema/bucket 719 | function CompiledModel(doc) { 720 | if (!(this instanceof CompiledModel)) 721 | return new CompiledModel(doc); 722 | 723 | Model.call(this, doc); 724 | } 725 | 726 | CompiledModel.constructor = CompiledModel; 727 | CompiledModel.constructor.name = name; 728 | 729 | CompiledModel.__proto__ = Model; 730 | CompiledModel.prototype.__proto__ = Model.prototype; 731 | CompiledModel.model = Model.prototype.model; 732 | CompiledModel.base = CompiledModel.prototype.base = base; 733 | CompiledModel.bucketName = CompiledModel.prototype.bucketName = bucketName; 734 | CompiledModel.modelName = CompiledModel.prototype.modelName = name; 735 | CompiledModel.schema = CompiledModel.prototype.schema = schema; 736 | CompiledModel.bucket = CompiledModel.prototype.bucket = bucket; 737 | 738 | initSchema(schema, schema.tree, CompiledModel.prototype); 739 | 740 | // apply default statics 741 | var k = null; 742 | for (k in Model.statics) { 743 | if (CompiledModel[k]) throw new Error('Unable to add default static method "' + k + '", already exists.'); 744 | else CompiledModel[k] = Model.statics[k]; 745 | } 746 | 747 | // apply the methods 748 | k = null; 749 | for (k in schema.methods) { 750 | if (CompiledModel.prototype[k]) { 751 | throw new Error('Unable to add method "' + k + '", already exists on prototype.'); 752 | } 753 | else { 754 | CompiledModel.prototype[k] = schema.methods[k]; 755 | } 756 | } 757 | 758 | // apply the statics 759 | k = null; 760 | for (k in schema.statics) { 761 | if (CompiledModel[k]) throw new Error('Unable to add static "' + k + '", already exists.'); 762 | else CompiledModel[k] = schema.statics[k]; 763 | } 764 | 765 | return CompiledModel; 766 | }; 767 | 768 | function initSchema(schema, tree, proto, prefix) { 769 | var keys = Object.keys(tree), 770 | i = keys.length, 771 | useLimb, 772 | limb, 773 | key; 774 | 775 | while (i--) { 776 | key = keys[i]; 777 | limb = tree[key]; 778 | 779 | useLimb = (limb.constructor.name === 'Object' && Object.keys(limb).length) && (!limb.type || limb.type.type); 780 | 781 | define( 782 | schema, 783 | key, 784 | (useLimb ? limb : null), 785 | proto, 786 | prefix, 787 | keys 788 | ); 789 | } 790 | } 791 | 792 | function define(schema, prop, subprops, prototype, prefix, keys) { 793 | prefix = prefix || ''; 794 | 795 | var path = (prefix ? prefix + '.' : '') + prop; 796 | 797 | if (subprops) { 798 | Object.defineProperty(prototype, prop, { 799 | enumerable: true, 800 | get: function () { 801 | if (!this._cache.getters) 802 | this._cache.getters = {}; 803 | 804 | if (!this._cache.getters[path]) { 805 | var nested = Object.create(this), 806 | i = 0, 807 | len = keys.length; 808 | 809 | if (!prefix) nested._cache.scope = this; 810 | 811 | for (; i < len; ++i) { 812 | //over-write parents getter without triggering it 813 | Object.defineProperty(nested, keys[i], { 814 | enumerable: false, // It doesn't show up. 815 | writable: true, // We can set it later. 816 | configurable: true, // We can Object.defineProperty again. 817 | value: undefined // It shadows its parent. 818 | }); 819 | } 820 | 821 | nested.toObject = function () { 822 | return this.get(path); 823 | }; 824 | 825 | initSchema(schema, subprops, nested, path); 826 | this._cache.getters[path] = nested; 827 | } 828 | 829 | return this._cache.getters[path]; 830 | }, 831 | set: function (v) { 832 | if (v instanceof Model) v = v.toObject(); 833 | return (this._cache.scope || this).set(path, v); 834 | } 835 | }); 836 | } else { 837 | Object.defineProperty(prototype, prop, { 838 | enumerable: true, 839 | get: function () { 840 | return this.get.call(this._cache.scope || this, path); 841 | }, 842 | set: function (v) { 843 | return this.set.call(this._cache.scope || this, path, v); 844 | } 845 | }); 846 | } 847 | } 848 | 849 | // function decorate(self, doc) { 850 | // Object.keys(self.schema.paths).forEach(function (path) { 851 | // addPath(path, self, doc, false); 852 | // }); 853 | 854 | // Object.keys(self.schema.virtuals).forEach(function (path) { 855 | // addPath(path, self, doc, true); 856 | // }); 857 | // } 858 | 859 | // function addPath(path, self, doc, virtual) { 860 | // utils.walk(path, self, function (obj, key) { 861 | // Object.defineProperty(obj, key, { 862 | // get: function () { 863 | // var val = virtual ? null : utils.getValue(path, doc); 864 | // return self.schema.path(path).applyGetters(val, this); 865 | // }, 866 | // set: function (v) { 867 | // return self.schema.path(path).applySetters(v, this); 868 | // } 869 | // }); 870 | // }); 871 | // } 872 | 873 | function buildDefaultDocument() { 874 | var doc = {}, 875 | paths = Object.keys(this.schema.paths), 876 | plen = paths.length; 877 | 878 | for (var ii = 0; ii < plen; ++ii) { 879 | var p = paths[ii], 880 | type = this.schema.paths[p], 881 | path = p.split('.'), 882 | len = path.length, 883 | last = len - 1, 884 | doc_ = doc; 885 | 886 | for (var i = 0; i < len; ++i) { 887 | var piece = path[i], 888 | def; 889 | 890 | if (i === last) { 891 | def = type.getDefault(this); 892 | if (def !== undefined) { 893 | doc_[piece] = def; 894 | } 895 | } else { 896 | doc_ = doc_[piece] || (doc_[piece] = {}); 897 | } 898 | } 899 | } 900 | 901 | return doc; 902 | } 903 | 904 | 905 | /** 906 | * Minimizes an object, removing undefined values and empty objects 907 | * 908 | * @param {Object} object to minimize 909 | * @return {Object} 910 | */ 911 | function minimize (obj) { 912 | var keys = Object.keys(obj), 913 | i = keys.length, 914 | hasKeys, 915 | key, 916 | val; 917 | 918 | while (i--) { 919 | key = keys[i]; 920 | val = obj[key]; 921 | 922 | if (utils.isObject(val)) { 923 | obj[key] = minimize(val); 924 | } 925 | 926 | if (obj[key] === undefined) { 927 | delete obj[key]; 928 | continue; 929 | } 930 | 931 | hasKeys = true; 932 | } 933 | 934 | return hasKeys ? obj : undefined; 935 | } 936 | 937 | /** 938 | * Applies properties to `json`. 939 | * 940 | * @param {Document} self 941 | * @param {Object} json 942 | * @param {String} type either `virtuals` or `paths` 943 | * @return {Object} `json` 944 | */ 945 | function applyGetters (self, json, type, options) { 946 | var schema = self.schema, 947 | paths = Object.keys(schema[type]), 948 | i = paths.length, 949 | path; 950 | 951 | while (i--) { 952 | path = paths[i]; 953 | 954 | var parts = path.split('.'), 955 | plen = parts.length, 956 | last = plen - 1, 957 | branch = json, 958 | part; 959 | 960 | for (var ii = 0; ii < plen; ++ii) { 961 | part = parts[ii]; 962 | if (ii === last) { 963 | branch[part] = utils.clone(self.get(path), options); 964 | } else { 965 | branch = branch[part] || (branch[part] = {}); 966 | } 967 | } 968 | } 969 | 970 | return json; 971 | } 972 | 973 | module.exports = Model; 974 | --------------------------------------------------------------------------------