octonom
Version:
Object Document Mapper for any database
787 lines (688 loc) • 33.1 kB
text/typescript
import { spy } from 'sinon';
import { collections } from '../test/data/collections';
import { AllModel, nestedCollection, NestedModel } from '../test/data/models/all';
import { CatModel } from '../test/data/models/cat';
import { DiscussionModel } from '../test/data/models/discussion';
import { GroupWithArrayModel } from '../test/data/models/group-with-array';
import { GroupWithReferencesModel } from '../test/data/models/group-with-references';
import { PersonModel } from '../test/data/models/person';
import { PersonAccountModel } from '../test/data/models/person-account';
import { ValidationError } from './errors';
import { Hook, Model } from './model';
import { Property, Schema, SchemaMap } from './schema/index';
describe('Hook decorator', () => {
let beforeObj;
let afterObj;
const beforeChange = spy(options => beforeObj = options.modelInstance.toObject());
const afterChange = spy(options => afterObj = options.modelInstance.toObject());
function resetSpies() {
beforeObj = undefined;
afterObj = undefined;
beforeChange.resetHistory();
afterChange.resetHistory();
}
beforeEach(resetSpies);
@Hook('beforeChange', beforeChange)
@Hook('afterChange', afterChange)
class Hooked extends Model {
@Property.String()
public foo: string;
@Property.String()
public baz: string;
@Property.Model({model: NestedModel})
public model: Partial<NestedModel>;
@Property.Object({schemaMap: {name: new Schema.String()}})
public object: {name: string};
@Property.Array({elementSchema: new Schema.String()})
public array: string[];
}
it('should register handlers', () => {
expect((Hooked.hooks as any).handlers.beforeChange).to.eql([beforeChange]);
expect((Hooked.hooks as any).handlers.afterChange).to.eql([afterChange]);
});
it('should create separate Hook instances for new classes', () => {
@Hook('beforeChange', beforeChange)
class DoubleHooked extends Hooked {}
expect((Hooked.hooks as any).handlers.beforeChange).to.eql([beforeChange]);
expect((Hooked.hooks as any).handlers.afterChange).to.eql([afterChange]);
expect((DoubleHooked.hooks as any).handlers.beforeChange).to.eql([beforeChange, beforeChange]);
expect((DoubleHooked.hooks as any).handlers.afterChange).to.eql([afterChange]);
});
describe('set handlers', () => {
describe('on root model', () => {
it('should run handlers when constructed', () => {
const hooked = new Hooked({foo: 'bar', object: {name: 'foo'}});
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar', object: {name: 'foo'}}});
expect(beforeObj).to.eql({});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar', object: {name: 'foo'}}});
expect(afterObj).to.eql({foo: 'bar', object: {name: 'foo'}});
});
it('should run handlers when calling set()', () => {
const hooked = new Hooked({});
resetSpies();
hooked.set({foo: 'bar', baz: 'lol'});
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar', baz: 'lol'}});
expect(beforeObj).to.eql({});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar', baz: 'lol'}});
expect(afterObj).to.eql({foo: 'bar', baz: 'lol'});
});
it('should run handlers when setting a value', () => {
const hooked = new Hooked({});
resetSpies();
hooked.foo = 'bar';
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar'}});
expect(beforeObj).to.eql({});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: 'bar'}});
expect(afterObj).to.eql({foo: 'bar'});
});
it('should run handlers when deleting a value', () => {
const hooked = new Hooked({foo: 'bar'});
resetSpies();
delete hooked.foo;
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: undefined}});
expect(beforeObj).to.eql({foo: 'bar'});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: [], value: {foo: undefined}});
expect(afterObj).to.eql({});
});
});
describe('nested model', () => {
it('should run handlers on root model when calling set() on nested model', () => {
const hooked = new Hooked({model: {name: 'foo'}});
resetSpies();
hooked.model.set({name: 'bar'});
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: 'bar'}});
expect(beforeObj).to.eql({model: {name: 'foo'}});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: 'bar'}});
expect(afterObj).to.eql({model: {name: 'bar'}});
});
it('should run handlers on root model when setting a value on a nested model', () => {
const hooked = new Hooked({model: {name: 'foo'}});
resetSpies();
hooked.model.name = 'bar';
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: 'bar'}});
expect(beforeObj).to.eql({model: {name: 'foo'}});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: 'bar'}});
expect(afterObj).to.eql({model: {name: 'bar'}});
});
it('should run handlers on root model when deleting a value on a nested model ', () => {
const hooked = new Hooked({model: {name: 'foo'}});
resetSpies();
delete hooked.model.name;
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: undefined}});
expect(beforeObj).to.eql({model: {name: 'foo'}});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['model'], value: {name: undefined}});
expect(afterObj).to.eql({model: {}});
});
});
describe('nested object', () => {
it('should run handlers on root model when setting a value on a nested object', () => {
const hooked = new Hooked({object: {name: 'foo'}});
resetSpies();
hooked.object.name = 'bar';
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['object'], value: {name: 'bar'}});
expect(beforeObj).to.eql({object: {name: 'foo'}});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['object'], value: {name: 'bar'}});
expect(afterObj).to.eql({object: {name: 'bar'}});
});
it('should run handlers on root model when deleting a value on a nested object', () => {
const hooked = new Hooked({object: {name: 'foo'}});
resetSpies();
delete hooked.object.name;
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['object'], value: {name: undefined}});
expect(beforeObj).to.eql({object: {name: 'foo'}});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['object'], value: {name: undefined}});
expect(afterObj).to.eql({object: {}});
});
});
describe('nested array', () => {
it('should run handlers on root model when setting a value on a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
hooked.array[1] = 'baz';
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array', 1], value: 'baz'});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array', 1], value: 'baz'});
expect(afterObj).to.eql({array: ['foo', 'baz']});
});
it('should run handlers on root model when deleting a value on a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
delete hooked.array[1];
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array', 1], value: undefined});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array', 1], value: undefined});
expect(afterObj).to.eql({array: ['foo', undefined]});
});
it.skip('should run handlers on root model when copying within an array', () => {
const hooked = new Hooked({array: ['foo', 'bar', 'baz']});
resetSpies();
hooked.array.copyWithin(2, 0, 1);
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'bar', 'foo']});
expect(beforeObj).to.eql({array: ['foo', 'bar', 'baz']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'bar', 'foo']});
expect(afterObj).to.eql({array: ['foo', 'bar', 'foo']});
});
it.skip('should run handlers on root model when filling a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar', 'boo']});
resetSpies();
hooked.array.fill('baz', 1, 3);
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'baz', 'baz']});
expect(beforeObj).to.eql({array: ['foo', 'bar', 'boo']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'baz', 'baz']});
expect(afterObj).to.eql({array: ['foo', 'baz', 'baz']});
});
it('should run handlers on root model when calling pop() on a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
const element = hooked.array.pop();
expect(element).to.equal('bar');
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo']});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo']});
expect(afterObj).to.eql({array: ['foo']});
});
it('should run handlers on root model when pushing values to a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
hooked.array.push('baz', 'boo');
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'bar', 'baz', 'boo']});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'bar', 'baz', 'boo']});
expect(afterObj).to.eql({array: ['foo', 'bar', 'baz', 'boo']});
});
it('should run handlers on root model when reversing a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
hooked.array.reverse();
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['bar', 'foo']});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['bar', 'foo']});
expect(afterObj).to.eql({array: ['bar', 'foo']});
});
it('should run handlers on root model when splicing a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar', 'baz']});
resetSpies();
hooked.array.splice(1, 1, 'boo', 'buu');
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'boo', 'buu', 'baz']});
expect(beforeObj).to.eql({array: ['foo', 'bar', 'baz']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['foo', 'boo', 'buu', 'baz']});
expect(afterObj).to.eql({array: ['foo', 'boo', 'buu', 'baz']});
});
it('should run handlers on root model when unshifting a nested array', () => {
const hooked = new Hooked({array: ['foo', 'bar']});
resetSpies();
hooked.array.unshift('baz', 'boo');
expect(beforeChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['baz', 'boo', 'foo', 'bar']});
expect(beforeObj).to.eql({array: ['foo', 'bar']});
expect(afterChange).to.be.calledOnce.and
.calledWithMatch({modelInstance: hooked, path: ['array'], value: ['baz', 'boo', 'foo', 'bar']});
expect(afterObj).to.eql({array: ['baz', 'boo', 'foo', 'bar']});
});
});
});
});
describe('Property decorator', () => {
class TestModel extends Model {
@Property.String()
public foo: string;
}
it('should register a schema property', () => {
const schemaMap = TestModel.schemaMap as SchemaMap<TestModel>;
expect(schemaMap.foo).to.be.an.instanceOf(Schema.String);
});
it('should create a separate for new classes', () => {
class Derived extends TestModel {
@Property.String()
public bar: string;
}
const schemaMap = TestModel.schemaMap as SchemaMap<TestModel>;
const derivedSchemaMap = Derived.schemaMap as SchemaMap<Derived>;
expect(schemaMap).to.not.have.property('bar');
expect(derivedSchemaMap.bar).to.be.an.instanceOf(Schema.String);
});
});
describe('Model', () => {
it('should allow non-schema properties', () => {
class TestModel extends Model {
@Property.String()
public foo: string;
public bar: string;
}
const instance = new TestModel({foo: 'test'});
instance.bar = 'baz';
expect(instance).to.eql({foo: 'test', bar: 'baz'});
delete instance.bar;
expect(instance).to.eql({foo: 'test'});
});
it('should set a schema property', () => {
class TestModel extends Model {
public foo: string;
}
const schema = new Schema.String();
TestModel.setSchema('foo', schema);
expect(TestModel.schemaMap).to.have.property('foo', schema);
});
it('should throw if a schema property is already set', () => {
class TestModel extends Model {
public foo: string;
}
TestModel.setSchema('foo', new Schema.String());
expect(() => TestModel.setSchema('foo', new Schema.String()))
.to.throw(Error, 'Key foo is already set.');
});
describe('simple (CatModel)', () => {
describe('constructor', () => {
it('should create an empty instance with generated id via default function', () => {
const cat = new CatModel();
expect(cat.id).to.be.a('string').of.length.at.least(1);
});
it('should create a cat with id and name', () => {
const cat = new CatModel({id: '42', name: 'Yllim'});
expect(cat.id).to.equal('42');
expect(cat.name).to.equal('Yllim');
});
});
describe('property setter', () => {
it('should set a property', () => {
const cat = new CatModel();
cat.id = '42';
expect(cat.id).to.equal('42');
cat.name = 'Yllim';
expect(cat.name).to.equal('Yllim');
});
it('should delete a property', () => {
const cat = new CatModel();
cat.id = '42';
delete cat.id;
expect(cat.id).to.equal(undefined);
});
});
describe('toObject()', () => {
it('should get only schema properties', () => {
const cat = new CatModel({id: '42', name: 'Yllim'});
expect(cat.toObject()).to.eql({id: '42', name: 'Yllim'});
});
it('should not return properties with undefined value', () => {
const cat = new CatModel({id: '42'});
expect(cat.toObject()).to.eql({id: '42'});
});
});
describe('inspect()', () => {
it('should equal toObject() result', () => {
const cat = new CatModel({id: '42', name: 'Yllim'});
expect(cat.inspect()).to.eql({id: '42', name: 'Yllim'});
});
});
});
describe('nested model (PersonAccountModel in PersonModel)', () => {
describe('constructor', () => {
it('should create a person without account', () => {
const person = new PersonModel({id: '42', name: 'Alice'});
expect(person.toJSON()).to.deep.equal({id: '42', name: 'Alice'});
});
it('should create a person with raw account object', () => {
const person = new PersonModel({id: '42', name: 'Alice', account: {username: 'alice'}});
expect(person.account).to.be.an.instanceOf(PersonAccountModel);
expect(person.toJSON()).to.deep.equal({id: '42', name: 'Alice', account: {username: 'alice'}});
});
it('should create a person with account instance', () => {
const account = new PersonAccountModel({username: 'alice'});
const person = new PersonModel({id: '42', name: 'Alice', account});
expect(person.account).to.be.an.instanceOf(PersonAccountModel).and.eql(account);
expect(person.toJSON()).to.deep.equal({id: '42', name: 'Alice', account: {username: 'alice'}});
});
});
describe('set()', () => {
it('should set a raw account object with set()', () => {
const person = new PersonModel({name: 'Alice'});
person.set({account: {username: 'alice'}});
expect(person.account).to.be.an.instanceOf(PersonAccountModel);
const personObj = person.toObject();
expect(personObj).to.deep.equal({name: 'Alice', account: {username: 'alice'}});
});
it('should set an account instance with set()', () => {
const person = new PersonModel({name: 'Alice'});
const account = new PersonAccountModel({username: 'alice'});
person.set({account});
expect(person.account).to.be.an.instanceOf(PersonAccountModel).and.eql(account);
const personObj = person.toObject();
expect(personObj).to.deep.equal({name: 'Alice', account: {username: 'alice'}});
});
});
describe('property setter', () => {
it('should set a raw account object directly', () => {
const person = new PersonModel({name: 'Alice'});
person.account = {username: 'alice'};
expect(person.account).to.be.an.instanceOf(PersonAccountModel);
const personObj = person.toObject();
expect(personObj).to.deep.equal({name: 'Alice', account: {username: 'alice'}});
});
it('should set an account instance directly', () => {
const person = new PersonModel({name: 'Alice'});
const account = new PersonAccountModel({username: 'alice'});
person.account = account;
expect(person.account).to.be.an.instanceOf(PersonAccountModel).and.eql(account);
const personObj = person.toObject();
expect(personObj).to.deep.equal({name: 'Alice', account: {username: 'alice'}});
});
});
});
describe('model array', () => {
describe('constructor', () => {
it('should create a group with a raw array with mixed raw person object and person instance', () => {
const person = new PersonModel({name: 'Bob'});
const group = new GroupWithArrayModel({members: [{name: 'Alice'}, person]});
expect(group.members).to.be.an('array').and.have.lengthOf(2);
expect(group.members[0]).to.be.an.instanceOf(PersonModel).and.have.property('name', 'Alice');
expect(group.members[1]).to.eql(person);
});
it('should create a group with a model array with person instances', () => {
const members = [{id: 'alice', name: 'Alice'}, {id: 'bob', name: 'Bob'}];
const group = new GroupWithArrayModel({members});
expect(group.members).to.eql(members);
});
});
describe('set()', () => {
it('should set a raw array with mixed raw person object and person instance', () => {
const person = new PersonModel({name: 'Bob'});
const group = new GroupWithArrayModel();
group.set({members: [{name: 'Alice'}, person]});
expect(group.members).to.be.an('array').and.have.lengthOf(2);
group.members.forEach(member => expect(member).to.be.an.instanceOf(PersonModel));
expect(group.members).to.containSubset([{name: 'Alice'}, person]);
});
it('should set a model array with person instances', () => {
const members = [{name: 'Alice'}, {name: 'Bob'}];
const group = new GroupWithArrayModel();
group.set({members});
group.members.forEach(member => expect(member).to.be.an.instanceOf(PersonModel));
expect(group.members).to.containSubset(members);
});
});
describe('property setter', () => {
it('should set a raw array with mixed raw person object and person instance', () => {
const person = new PersonModel({name: 'Bob'});
const group = new GroupWithArrayModel();
group.members = [{name: 'Alice'}, person];
expect(group.members).to.be.an('array').and.to.have.lengthOf(2);
expect(group.members[0]).to.be.an.instanceOf(PersonModel).and.have.property('name', 'Alice');
expect(group.members[1]).to.eql(person);
});
it('should set a model array with a person instance', () => {
const members = [{name: 'Alice'}, {name: 'Bob'}];
const group = new GroupWithArrayModel();
group.members = members;
expect(group.members).to.eql(members);
});
});
describe('toObject()', () => {
it('should run toObject on array elements', async () => {
const person = new PersonModel({id: '42', name: 'Bob'});
const group = new GroupWithArrayModel({id: '1337', members: [person]});
expect(group.toObject({unpopulate: true})).to.eql({id: '1337', members: [{id: '42', name: 'Bob'}]});
});
});
});
describe('reference (author/PersonModel in DiscussionModel', () => {
const alice = new PersonModel({id: '42', name: 'Alice'});
const bob = new PersonModel({id: '23', name: 'Bob'});
beforeEach(() => {
collections.people.clear();
collections.people.insert(alice);
collections.people.insert(bob);
});
describe('constructor', () => {
it('should create a discussion with an id', () => {
const discussion = new DiscussionModel({author: '42'});
expect(discussion.author).to.equal('42');
});
it('should create a discussion with a person instance', () => {
const discussion = new DiscussionModel({author: alice});
expect(discussion.author).to.eql(alice).and.to.not.equal(alice);
});
});
describe('set()', () => {
it('should set a discussion with an id', () => {
const discussion = new DiscussionModel();
discussion.set({author: '42'});
expect(discussion.author).to.equal('42');
});
it('should set a discussion with a person instance', () => {
const discussion = new DiscussionModel();
discussion.set({author: alice});
expect(discussion.author).to.eql(alice).and.to.not.equal(alice);
});
});
describe('property setter', () => {
it('should set a discussion with an id', () => {
const discussion = new DiscussionModel();
discussion.author = '42';
expect(discussion.author).to.equal('42');
});
it('should set a discussion with a person instance', () => {
const discussion = new DiscussionModel();
discussion.author = alice;
expect(discussion.author).to.eql(alice).and.to.not.equal(alice);
});
});
describe('populate()', () => {
it('should throw if key does not exist', async () => {
const discussion = new DiscussionModel();
await expect(discussion.populate({'non-existent': true} as any))
.to.be.rejectedWith('Key non-existent not found in schema');
});
it('should use undefined if id does not exist', async () => {
const discussion = new DiscussionModel({id: 'id', author: 'non-existent'});
const result = await discussion.populate({author: true});
expect(result).to.equal(discussion).and.to.eql({id: 'id', author: undefined});
});
it('should populate an id with an instance', async () => {
const discussion = new DiscussionModel({author: '42'});
await discussion.populate({author: true});
expect(discussion.author).to.be.instanceof(PersonModel);
expect((discussion.author as PersonModel).toObject()).to.eql({id: '42', name: 'Alice'});
});
});
describe('toObject()', () => {
it('should return the id of an unpopulated reference', () => {
const discussion = new DiscussionModel({author: '42'});
expect(discussion.toObject()).have.property('author', '42');
});
it('should run toObject() recursively on a populated reference with {unpopulate: false}', () => {
const discussion = new DiscussionModel({author: alice});
expect(discussion.toObject({unpopulate: false})).have.property('author').that.eql({id: '42', name: 'Alice'});
});
it('should return the id of a populated reference with {unpopulate: true}', () => {
const discussion = new DiscussionModel({author: alice});
expect(discussion.toObject({unpopulate: true})).have.property('author', '42');
});
it('should return the id of an reference that is populated with undefined', async () => {
const discussion = new DiscussionModel({id: 'id', author: 'non-existent'});
await discussion.populate({author: true});
expect(discussion.toObject({unpopulate: true})).to.eql({id: 'id', author: 'non-existent'});
});
});
});
describe('reference array', () => {
const alice = new PersonModel({id: '42', name: 'Alice'});
const bob = new PersonModel({id: '23', name: 'Bob'});
beforeEach(() => {
collections.people.clear();
collections.people.insert(alice);
collections.people.insert(bob);
});
describe('constructor', () => {
it('should create a group with an array with id and person instance', () => {
const members = [alice.id, bob];
const group = new GroupWithReferencesModel({id: '1337', members});
expect(group.members).to.be.an('array').and.to.eql(members);
});
});
describe('set()', () => {
it('should set an array with id and person instance', () => {
const group = new GroupWithReferencesModel({id: '1337'});
const members = [alice.id, bob];
group.set({members});
expect(group.members).to.be.an('array').and.to.eql(members);
});
});
describe('property setter', () => {
it('should set an array with id and person instance', () => {
const group = new GroupWithReferencesModel({id: '1337'});
const members = [alice.id, bob];
group.members = members;
expect(group.members).to.be.an('array').and.to.eql(members);
});
});
describe('populate()', () => {
it('should throw if path is invalid', async () => {
const group = new GroupWithReferencesModel({id: '1337', members: [alice]});
await expect(group.populate({members: {foo: true}})).to.be.rejectedWith('Key foo not found in schema');
});
it('should use undefined if id does not exist', async () => {
const group = new GroupWithReferencesModel({id: '1337', members: ['non-existent']});
const result = await group.populate({members: true});
expect(result).to.equal(group).and.to.eql({id: '1337', members: [undefined]});
});
it('should populate ids with an instance', async () => {
const group = new GroupWithReferencesModel({id: '1337', members: [alice, bob.id]});
await group.populate({members: true});
expect(group.members).to.be.an('array').and.be.of.length(2);
group.members.forEach(member => expect(member).to.be.an.instanceOf(PersonModel));
expect(group.members).to.containSubset([alice, bob]);
});
});
describe('toObject()', () => {
it('should return an object for instances and the id for an unpopulated reference', () => {
const members = [alice, bob.id];
const group = new GroupWithReferencesModel({id: '1337', members});
const groupObj = group.toObject();
expect(groupObj.members).to.eql([alice.toObject(), bob.id]);
});
it('should run toObject() recursively on populated references with {unpopulate: false}', () => {
const members = [alice, bob.id];
const group = new GroupWithReferencesModel({id: '1337', members});
const groupObj = group.toObject({unpopulate: false});
expect(groupObj.members).to.eql([alice.toObject(), bob.id]);
});
it('should return the id of populated references with {unpopulate: true}', () => {
const members = [alice, bob.id];
const group = new GroupWithReferencesModel({id: '1337', members});
const groupObj = group.toObject({unpopulate: true});
expect(groupObj.members).to.eql([alice.id, bob.id]);
});
it('should return the id of a reference that is populated with undefined', async () => {
const group = new GroupWithReferencesModel({id: '1337', members: ['non-existent']});
await group.populate({members: true});
expect(group.toObject({unpopulate: true})).to.eql({id: '1337', members: ['non-existent']});
});
});
});
describe('all schemas (AllModel)', () => {
const nested = new NestedModel({id: 'alice', name: 'Alice'});
before(() => {
nestedCollection.clear();
nestedCollection.insert(nested);
});
it('should create an instance with all schemas', async () => {
const data: Partial<AllModel> = {
any: {foo: 'bar'},
array: [true, false],
boolean: false,
date: new Date(),
model: new NestedModel({id: 'bob', name: 'Bob'}),
number: 42,
object: {enabled: true},
reference: nested.id,
string: 'foo',
};
const all = new AllModel(data);
expect(all).to.eql(data);
});
});
describe('validate()', () => {
class TestModel extends Model {
@Property.String({required: true})
public required: string;
@Property.Array({elementSchema: new Schema.String({enum: ['foo', 'bar']})})
public array: string[];
@Property.Model({model: TestModel})
public model: TestModel;
@Property.Object({schemaMap: {foo: new Schema.String({enum: ['bar']})}})
public object: {foo: string};
}
it('should throw if a key is required', async () => {
const instance = new TestModel();
await expect(instance.validate())
.to.be.rejectedWith(ValidationError, 'Key required is required.');
});
it('should throw if nested array validation throws', async () => {
const instance = new TestModel({required: 'foo', array: ['foo', 'baz']});
let error;
await instance.validate().catch(e => error = e);
expect(error).to.be.instanceOf(ValidationError);
expect(error.message).to.equal('String not in enum: foo, bar.');
});
it('should throw if nested model validation throws', async () => {
const instance = new TestModel({
required: 'foo',
model: new TestModel({}),
});
let error;
await instance.validate().catch(e => error = e);
expect(error).to.be.instanceOf(ValidationError);
expect(error.message).to.equal('Key required is required.');
});
it('should throw if nested object validation throws', async () => {
const instance = new TestModel({required: 'foo', object: {foo: 'baz'}});
let error;
await instance.validate().catch(e => error = e);
expect(error).to.be.instanceOf(ValidationError);
expect(error.message).to.equal('String not in enum: bar.');
});
it('should pass with a valid instance', async () => {
const instance = new TestModel({
required: 'foo',
array: ['foo', 'bar'],
model: new TestModel({required: 'bar'}),
object: {foo: 'bar'},
});
await instance.validate();
});
});
});