UNPKG

camo

Version:

A class-based ES6 ODM for Mongo-like databases.

716 lines (591 loc) 23.3 kB
'use strict'; const _ = require('lodash'); const fs = require('fs'); const expect = require('chai').expect; const connect = require('../index').connect; const Document = require('../index').Document; const EmbeddedDocument = require('../index').EmbeddedDocument; const isDocument = require('../lib/validate').isDocument; const ValidationError = require('../lib/errors').ValidationError; const Data = require('./data'); const getData1 = require('./util').data1; const getData2 = require('./util').data2; const validateId = require('./util').validateId; describe('Embedded', function() { // TODO: Should probably use mock database client... const url = 'nedb://memory'; //const url = 'mongodb://localhost/camo_test'; let database = null; before(function(done) { connect(url).then(function(db) { database = db; return database.dropDatabase(); }).then(function() { return done(); }); }); beforeEach(function(done) { done(); }); afterEach(function(done) { database.dropDatabase().then(function() {}).then(done, done); }); after(function(done) { database.dropDatabase().then(function() {}).then(done, done); }); describe('general', function() { it('should not have an _id', function(done) { class EmbeddedModel extends EmbeddedDocument { constructor() { super(); this.str = String; } } class DocumentModel extends Document { constructor() { super(); this.mod = EmbeddedModel; this.num = { type: Number }; } } let data = DocumentModel.create(); data.mod = EmbeddedModel.create(); data.mod.str = 'some data'; data.num = 1; data.save().then(function() { expect(data.mod._id).to.be.undefined; return DocumentModel.findOne({ num: 1 }); }).then(function(d) { expect(d.mod._id).to.be.undefined; }).then(done, done); }); }); describe('types', function() { it('should allow embedded types', function(done) { class EmbeddedModel extends EmbeddedDocument { constructor() { super(); this.str = String; } } class DocumentModel extends Document { constructor() { super(); this.mod = EmbeddedModel; this.num = { type: Number }; } } let data = DocumentModel.create(); data.mod = EmbeddedModel.create(); data.mod.str = 'some data'; data.num = 1; data.save().then(function() { validateId(data); return DocumentModel.findOne({ num: 1 }); }).then(function(d) { validateId(d); expect(d.num).to.be.equal(1); expect(d.mod).to.be.a('object'); expect(d.mod).to.be.an.instanceof(EmbeddedModel); expect(d.mod.str).to.be.equal('some data'); }).then(done, done); }); it('should allow array of embedded types', function(done) { class Limb extends EmbeddedDocument { constructor() { super(); this.type = String; } } class Person extends Document { constructor() { super(); this.limbs = [Limb]; this.name = String; } static collectionName() { return 'people'; } } let person = Person.create(); person.name = 'Scott'; person.limbs.push(Limb.create()); person.limbs[0].type = 'left arm'; person.limbs.push(Limb.create()); person.limbs[1].type = 'right arm'; person.limbs.push(Limb.create()); person.limbs[2].type = 'left leg'; person.limbs.push(Limb.create()); person.limbs[3].type = 'right leg'; person.save().then(function() { validateId(person); expect(person.limbs).to.have.length(4); return Person.findOne({ name: 'Scott' }); }).then(function(p) { validateId(p); expect(p.name).to.be.equal('Scott'); expect(p.limbs).to.be.a('array'); expect(p.limbs).to.have.length(4); expect(p.limbs[0].type).to.be.equal('left arm'); expect(p.limbs[1].type).to.be.equal('right arm'); expect(p.limbs[2].type).to.be.equal('left leg'); expect(p.limbs[3].type).to.be.equal('right leg'); }).then(done, done); }); it('should save nested array of embeddeds', function(done) { class Point extends EmbeddedDocument { constructor() { super(); this.x = Number; this.y = Number; } } class Polygon extends EmbeddedDocument { constructor() { super(); this.points = [Point]; } } class WorldMap extends Document { constructor() { super(); this.polygons = [Polygon]; } } let map = WorldMap.create(); let polygon1 = Polygon.create(); let polygon2 = Polygon.create(); let point1 = Point.create({ x: 123.45, y: 678.90 }); let point2 = Point.create({ x: 543.21, y: 987.60 }); map.polygons.push(polygon1); map.polygons.push(polygon2); polygon2.points.push(point1); polygon2.points.push(point2); map.save().then(function() { return WorldMap.findOne(); }).then(function(m) { expect(m.polygons).to.have.length(2); expect(m.polygons[0]).to.be.instanceof(Polygon); expect(m.polygons[1]).to.be.instanceof(Polygon); expect(m.polygons[1].points).to.have.length(2); expect(m.polygons[1].points[0]).to.be.instanceof(Point); expect(m.polygons[1].points[1]).to.be.instanceof(Point); }).then(done, done); }); it('should allow nested initialization of embedded types', function(done) { class Discount extends EmbeddedDocument { constructor() { super(); this.authorized = Boolean; this.amount = Number; } } class Product extends Document { constructor() { super(); this.name = String; this.discount = Discount; } } let product = Product.create({ name: 'bike', discount: { authorized: true, amount: 9.99 } }); product.save().then(function() { validateId(product); expect(product.name).to.be.equal('bike'); expect(product.discount).to.be.a('object'); expect(product.discount instanceof Discount).to.be.true; expect(product.discount.authorized).to.be.equal(true); expect(product.discount.amount).to.be.equal(9.99); }).then(done, done); }); it('should allow initialization of array of embedded documents', function(done) { class Discount extends EmbeddedDocument { constructor() { super(); this.authorized = Boolean; this.amount = Number; } } class Product extends Document { constructor() { super(); this.name = String; this.discounts = [Discount]; } } let product = Product.create({ name: 'bike', discounts: [{ authorized: true, amount: 9.99 }, { authorized: false, amount: 187.44 }] }); product.save().then(function() { validateId(product); expect(product.name).to.be.equal('bike'); expect(product.discounts).to.have.length(2); expect(product.discounts[0] instanceof Discount).to.be.true; expect(product.discounts[1] instanceof Discount).to.be.true; expect(product.discounts[0].authorized).to.be.equal(true); expect(product.discounts[0].amount).to.be.equal(9.99); expect(product.discounts[1].authorized).to.be.equal(false); expect(product.discounts[1].amount).to.be.equal(187.44); }).then(done, done); }); }); describe('defaults', function() { it('should assign defaults to embedded types', function(done) { class EmbeddedModel extends EmbeddedDocument { constructor() { super(); this.str = { type: String, default: 'hello' }; } } class DocumentModel extends Document { constructor() { super(); this.emb = EmbeddedModel; this.num = { type: Number }; } } let data = DocumentModel.create(); data.emb = EmbeddedModel.create(); data.num = 1; data.save().then(function() { validateId(data); return DocumentModel.findOne({ num: 1 }); }).then(function(d) { validateId(d); expect(d.emb.str).to.be.equal('hello'); }).then(done, done); }); it('should assign defaults to array of embedded types', function(done) { class Money extends EmbeddedDocument { constructor() { super(); this.value = { type: Number, default: 100 }; } } class Wallet extends Document { constructor() { super(); this.contents = [Money]; this.owner = String; } } let wallet = Wallet.create(); wallet.owner = 'Scott'; wallet.contents.push(Money.create()); wallet.contents.push(Money.create()); wallet.contents.push(Money.create()); wallet.save().then(function() { validateId(wallet); return Wallet.findOne({ owner: 'Scott' }); }).then(function(w) { validateId(w); expect(w.owner).to.be.equal('Scott'); expect(w.contents[0].value).to.be.equal(100); expect(w.contents[1].value).to.be.equal(100); expect(w.contents[2].value).to.be.equal(100); }).then(done, done); }); }); describe('validate', function() { it('should validate embedded values', function(done) { class EmbeddedModel extends EmbeddedDocument { constructor() { super(); this.num = { type: Number, max: 10 }; } } class DocumentModel extends Document { constructor() { super(); this.emb = EmbeddedModel; } } let data = DocumentModel.create(); data.emb = EmbeddedModel.create(); data.emb.num = 26; data.save().then(function() { expect.fail(null, Error, 'Expected error, but got none.'); }).catch(function(error) { expect(error).to.be.instanceof(ValidationError); expect(error.message).to.contain('max'); }).then(done, done); }); it('should validate array of embedded values', function(done) { class Money extends EmbeddedDocument { constructor() { super(); this.value = { type: Number, choices: [1, 5, 10, 20, 50, 100] }; } } class Wallet extends Document { constructor() { super(); this.contents = [Money]; } } let wallet = Wallet.create(); wallet.contents.push(Money.create()); wallet.contents[0].value = 5; wallet.contents.push(Money.create()); wallet.contents[1].value = 26; wallet.save().then(function() { expect.fail(null, Error, 'Expected error, but got none.'); }).catch(function(error) { expect(error).to.be.instanceof(ValidationError); expect(error.message).to.contain('choices'); }).then(done, done); }); }); describe('canonicalize', function() { it('should ensure timestamp dates are converted to Date objects', function(done) { class Education extends EmbeddedDocument { constructor() { super(); this.school = String; this.major = String; this.dateGraduated = Date; } static collectionName() { return 'people'; } } class Person extends Document { constructor() { super(); this.gradSchool = Education; } static collectionName() { return 'people'; } } let now = new Date(); let person = Person.create({ gradSchool: { school: 'CMU', major: 'ECE', dateGraduated: now } }); person.save().then(function() { validateId(person); expect(person.gradSchool.school).to.be.equal('CMU'); expect(person.gradSchool.dateGraduated.getFullYear()).to.be.equal(now.getFullYear()); expect(person.gradSchool.dateGraduated.getHours()).to.be.equal(now.getHours()); expect(person.gradSchool.dateGraduated.getMinutes()).to.be.equal(now.getMinutes()); expect(person.gradSchool.dateGraduated.getMonth()).to.be.equal(now.getMonth()); expect(person.gradSchool.dateGraduated.getSeconds()).to.be.equal(now.getSeconds()); }).then(done, done); }); }); describe('hooks', function() { it('should call all pre and post functions on embedded models', function(done) { let preValidateCalled = false; let preSaveCalled = false; let preDeleteCalled = false; let postValidateCalled = false; let postSaveCalled = false; let postDeleteCalled = false; class Coffee extends EmbeddedDocument { constructor() { super(); } preValidate() { preValidateCalled = true; } postValidate() { postValidateCalled = true; } preSave() { preSaveCalled = true; } postSave() { postSaveCalled = true; } preDelete() { preDeleteCalled = true; } postDelete() { postDeleteCalled = true; } } class Cup extends Document { constructor() { super(); this.contents = Coffee; } } let cup = Cup.create(); cup.contents = Coffee.create(); cup.save().then(function() { validateId(cup); // Pre/post save and validate should be called expect(preValidateCalled).to.be.equal(true); expect(preSaveCalled).to.be.equal(true); expect(postValidateCalled).to.be.equal(true); expect(postSaveCalled).to.be.equal(true); // Pre/post delete should not have been called yet expect(preDeleteCalled).to.be.equal(false); expect(postDeleteCalled).to.be.equal(false); return cup.delete(); }).then(function(numDeleted) { expect(numDeleted).to.be.equal(1); expect(preDeleteCalled).to.be.equal(true); expect(postDeleteCalled).to.be.equal(true); }).then(done, done); }); it('should call all pre and post functions on array of embedded models', function(done) { let preValidateCalled = false; let preSaveCalled = false; let preDeleteCalled = false; let postValidateCalled = false; let postSaveCalled = false; let postDeleteCalled = false; class Money extends EmbeddedDocument { constructor() { super(); } preValidate() { preValidateCalled = true; } postValidate() { postValidateCalled = true; } preSave() { preSaveCalled = true; } postSave() { postSaveCalled = true; } preDelete() { preDeleteCalled = true; } postDelete() { postDeleteCalled = true; } } class Wallet extends Document { constructor() { super(); this.contents = [Money]; } } let wallet = Wallet.create(); wallet.contents.push(Money.create()); wallet.contents.push(Money.create()); wallet.save().then(function() { validateId(wallet); // Pre/post save and validate should be called expect(preValidateCalled).to.be.equal(true); expect(postValidateCalled).to.be.equal(true); expect(preSaveCalled).to.be.equal(true); expect(postSaveCalled).to.be.equal(true); // Pre/post delete should not have been called yet expect(preDeleteCalled).to.be.equal(false); expect(postDeleteCalled).to.be.equal(false); return wallet.delete(); }).then(function(numDeleted) { expect(numDeleted).to.be.equal(1); expect(preDeleteCalled).to.be.equal(true); expect(postDeleteCalled).to.be.equal(true); }).then(done, done); }); }); describe('serialize', function() { it('should serialize data to JSON', function(done) { class Address extends EmbeddedDocument { constructor() { super(); this.street = String; this.city = String; this.zipCode = Number; this.isPoBox = Boolean; } } class Person extends Document { constructor() { super(); this.name = String; this.age = Number; this.isAlive = Boolean; this.children = [String]; this.address = Address; } static collectionName() { return 'people'; } } let person = Person.create({ name: 'Scott', address: { street: '123 Fake St.', city: 'Cityville', zipCode: 12345, isPoBox: false } }); person.save().then(function() { validateId(person); expect(person.name).to.be.equal('Scott'); expect(person.address).to.be.an.instanceof(Address); expect(person.address.street).to.be.equal('123 Fake St.'); expect(person.address.city).to.be.equal('Cityville'); expect(person.address.zipCode).to.be.equal(12345); expect(person.address.isPoBox).to.be.equal(false); let json = person.toJSON(); expect(json.name).to.be.equal('Scott'); expect(json.address).to.not.be.an.instanceof(Address); expect(json.address.street).to.be.equal('123 Fake St.'); expect(json.address.city).to.be.equal('Cityville'); expect(json.address.zipCode).to.be.equal(12345); expect(json.address.isPoBox).to.be.equal(false); }).then(done, done); }); it('should serialize data to JSON and ignore methods', function(done) { class Address extends EmbeddedDocument { constructor() { super(); this.street = String; } getBar() { return 'bar'; } } class Person extends Document { constructor() { super(); this.name = String; this.address = Address; } static collectionName() { return 'people'; } getFoo() { return 'foo'; } } let person = Person.create({ name: 'Scott', address : { street : 'Bar street' } }); let json = person.toJSON(); expect(json).to.have.keys(['_id', 'name', 'address']); expect(json.address).to.have.keys(['street']); done(); }); }); });