lux-framework
Version:
Build scalable, Node.js-powered REST APIs with almost no code.
1,855 lines (1,458 loc) • 46.9 kB
JavaScript
// @flow
import { spy } from 'sinon';
import { expect } from 'chai';
import { it, describe, before, after, beforeEach, afterEach } from 'mocha';
import Model from '../model';
import Query, { RecordNotFoundError } from '../query';
import { ValidationError } from '../validation';
import setType from '../../../utils/set-type';
import { getTestApp } from '../../../../test/utils/get-test-app';
describe('module "database/model"', () => {
describe('class Model', () => {
let store;
let User: Class<Model>;
let Image: Class<Model>;
let Comment: Class<Model>;
before(async () => {
const app = await getTestApp();
store = app.store;
// $FlowIgnore
User = app.models.get('user');
// $FlowIgnore
Image = app.models.get('image');
// $FlowIgnore
Comment = app.models.get('comment');
});
describe('.initialize()', () => {
class Subject extends Model {
static tableName = 'posts';
static belongsTo = {
user: {
inverse: 'posts'
}
};
static hasMany = {
comments: {
inverse: 'post'
},
reactions: {
inverse: 'post',
model: 'reaction'
},
tags: {
inverse: 'posts',
through: 'categorization'
}
};
static hooks = {
async afterCreate() {},
async beforeDestroy() {},
async duringDestroy() {}
// ^^^^^^^^^^^^^ This hook should be removed.
};
static scopes = {
isPublic() {
return this.where({
isPublic: true
});
},
isDraft() {
return this.where({
isPublic: false
});
}
};
static validates = {
title: str => Boolean(str),
notAFunction: {},
//^^^^^^^^^^^^ This validation should be removed.
notAnAttribute: () => false
//^^^^^^^^^^^^^^ This validation should be removed.
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('can be called repeatedly without error', async () => {
const table = () => store.connection(Subject.tableName);
const refs = await Promise.all([
Subject.initialize(store, table),
Subject.initialize(store, table),
Subject.initialize(store, table)
]);
refs.forEach(ref => {
expect(ref).to.equal(Subject);
});
});
it('adds a `store` property to the `Model`', () => {
expect(Subject).to.have.property('store', store);
});
it('adds a `table` property to the `Model`', () => {
expect(Subject)
.to.have.property('table')
.and.be.a('function');
});
it('adds a `logger` property to the `Model`', () => {
expect(Subject).to.have.property('logger', store.logger);
});
it('adds an `attributes` property to the `Model`', () => {
expect(Subject)
.to.have.property('attributes')
.and.have.all.keys([
'id',
'body',
'title',
'isPublic',
'userId',
'createdAt',
'updatedAt'
]);
Object.keys(Subject.attributes).forEach(key => {
const value = Reflect.get(Subject.attributes, key);
expect(value).to.have.all.keys([
'type',
'docName',
'nullable',
'maxLength',
'columnName',
'defaultValue'
]);
});
});
it('adds an `attributeNames` property to the `Model`', () => {
expect(Subject)
.to.have.property('attributeNames')
.and.include.all.members([
'id',
'body',
'title',
'isPublic',
'userId',
'createdAt',
'updatedAt'
]);
});
it('adds attribute accessors on the `prototype`', () => {
Object.keys(Subject.attributes).forEach(key => {
const desc = Reflect.getOwnPropertyDescriptor(Subject.prototype, key);
expect(desc).to.have.property('get').and.be.a('function');
expect(desc).to.have.property('set').and.be.a('function');
});
});
it('adds a `hasOne` property to the `Model`', () => {
expect(Subject.hasOne).to.deep.equal({});
});
it('adds a `hasMany` property to the `Model`', () => {
expect(Subject.hasMany).to.have.all.keys([
'tags',
'comments',
'reactions'
]);
Object.keys(Subject.hasMany).forEach(key => {
const value = Reflect.get(Subject.hasMany, key);
expect(value).to.be.an('object');
expect(value).to.have.property('type').and.equal('hasMany');
expect(Reflect.ownKeys(value)).to.include.all.members([
'type',
'model',
'inverse',
'through',
'foreignKey'
]);
});
});
it('adds a `belongsTo` property to the `Model`', () => {
expect(Subject.belongsTo).to.have.all.keys(['user']);
Object.keys(Subject.belongsTo).forEach(key => {
const value = Reflect.get(Subject.belongsTo, key);
expect(value).to.be.an('object');
expect(value).to.have.property('type').and.equal('belongsTo');
expect(Reflect.ownKeys(value)).to.include.all.members([
'type',
'model',
'inverse',
'foreignKey'
]);
});
});
it('adds a `relationships` property to the `Model`', () => {
expect(Subject.relationships).to.have.all.keys([
'user',
'tags',
'comments',
'reactions'
]);
Object.keys(Subject.relationships).forEach(key => {
const value = Reflect.get(Subject.relationships, key);
expect(value).to.have.property('type');
if (value.type === 'hasMany') {
expect(Reflect.ownKeys(value)).to.include.all.members([
'type',
'model',
'inverse',
'through',
'foreignKey'
]);
} else {
expect(Reflect.ownKeys(value)).to.include.all.members([
'type',
'model',
'inverse',
'foreignKey'
]);
}
});
});
it('adds a `relationshipNames` property to the `Model`', () => {
expect(Subject.relationshipNames).to.include.all.members([
'user',
'tags',
'comments',
'reactions'
]);
});
it('adds relationship accessors to the `prototype`', () => {
Object.keys(Subject.relationships).forEach(key => {
const desc = Reflect.getOwnPropertyDescriptor(Subject.prototype, key);
expect(desc).to.have.property('get').and.be.a('function');
expect(desc).to.have.property('set').and.be.a('function');
});
});
it('removes invalid hooks from the `hooks` property', () => {
expect(Subject)
.to.have.property('hooks')
.and.be.an('object')
.and.not.have.any.keys(['duringDestroy']);
expect(Subject)
.to.have.deep.property('hooks.afterCreate')
.and.be.a('function');
expect(Subject)
.to.have.deep.property('hooks.beforeDestroy')
.and.be.a('function');
});
it('adds each scope to `Model`', () => {
expect(Subject.scopes).to.have.all.keys([
'isDraft',
'isPublic'
]);
Object.keys(Subject.scopes).forEach(key => {
const value = Reflect.get(Subject, key);
expect(value).to.be.a('function');
});
});
it('removes invalid validations from the `validates` property', () => {
expect(Subject.validates).to.have.all.keys(['title']);
expect(Subject.validates.title).to.be.a('function');
});
it('adds a `modelName` property to the `Model`', () => {
expect(Subject).to.have.property('modelName', 'subject');
});
it('adds a `modelName` property to the `prototype`', () => {
expect(Subject).to.have.deep.property('prototype.modelName', 'subject');
});
it('adds a `resourceName` property to the `Model`', () => {
expect(Subject).to.have.property('resourceName', 'subjects');
});
it('adds a `resourceName` property to the `prototype`', () => {
expect(Subject)
.to.have.deep.property('prototype.resourceName', 'subjects');
});
it('adds an `initialized` property to the `Model`', () => {
expect(Subject.initialized).to.be.true;
});
describe('- without `tableName`', () => {
class Post extends Model {}
before(async () => {
await Post.initialize(store, () => {
return store.connection(Post.tableName);
});
});
it('adds a `tableName` property to the `prototype`', () => {
expect(Post).to.have.property('tableName', 'posts');
});
it('adds a `tableName` property to the `prototype`', () => {
expect(Post).to.have.deep.property('prototype.tableName', 'posts');
});
});
});
describe('.create()', () => {
let result: Subject;
class Subject extends Model {
static tableName = 'posts';
static belongsTo = {
user: {
inverse: 'posts'
}
};
}
beforeEach(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
afterEach(async () => {
await result.destroy();
});
it('constructs and persists a `Model` instance', async () => {
const user = new User({ id: 1 });
const body = 'Contents of "Test Post"...';
const title = 'Test Post';
result = await Subject.create({
user,
body,
title,
isPublic: true
});
expect(result).to.be.an.instanceof(Subject);
expect(result).to.have.property('id').and.be.a('number');
expect(result).to.have.property('body', body);
expect(result).to.have.property('title', title);
expect(result).to.have.property('isPublic', true);
expect(result).to.have.property('createdAt').and.be.an.instanceof(Date);
expect(result).to.have.property('updatedAt').and.be.an.instanceof(Date);
// $FlowIgnore
expect(await result.user).to.have.property('id', user.getPrimaryKey());
});
it('constructs and persists a `Model` instance with an integer id specified', async () => {
const id = 999;
result = await Subject.create({ id });
expect(result).to.be.an.instanceof(Subject);
expect(result).to.have.property('id', id);
});
});
describe('.transacting()', async () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns a static transaction proxy', async () => {
await Subject.transaction(trx => {
const proxy = Subject.transacting(trx);
expect(proxy.create).to.be.a('function');
return Promise.resolve(new Subject());
});
});
});
describe('.all()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.all();
expect(result).to.be.an.instanceof(Query);
});
});
describe('.find()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.find();
expect(result).to.be.an.instanceof(Query);
});
});
describe('.page()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.page(1);
expect(result).to.be.an.instanceof(Query);
});
});
describe('.limit()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.limit(25);
expect(result).to.be.an.instanceof(Query);
});
});
describe('.offset()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.offset(0);
expect(result).to.be.an.instanceof(Query);
});
});
describe('.count()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.count();
expect(result).to.be.an.instanceof(Query);
});
});
describe('.order()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.order('createdAt', 'ASC');
expect(result).to.be.an.instanceof(Query);
});
});
describe('.where()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.where({
isPublic: true
});
expect(result).to.be.an.instanceof(Query);
});
});
describe('.whereBetween()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.whereBetween({
userId: [1, 10]
});
expect(result).to.be.an.instanceof(Query);
});
});
describe('.whereRaw()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.whereRaw(
`"title" LIKE ?`,
[`%Test%`]
);
expect(result).to.be.an.instanceof(Query);
});
});
describe('.not()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.not({
isPublic: true
});
expect(result).to.be.an.instanceof(Query);
});
});
describe('.first()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.first();
expect(result).to.be.an.instanceof(Query);
});
});
describe('.last()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.last();
expect(result).to.be.an.instanceof(Query);
});
});
describe('.select()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.select('title', 'createdAt');
expect(result).to.be.an.instanceof(Query);
});
});
describe('.distinct()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.distinct('title');
expect(result).to.be.an.instanceof(Query);
});
});
describe('.include()', () => {
class Subject extends Model {
static tableName = 'posts';
static hasMany = {
comments: {
inverse: 'post'
}
};
static belongsTo = {
user: {
inverse: 'posts'
}
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.include('user', 'comments');
expect(result).to.be.an.instanceof(Query);
});
});
describe('.unscope()', () => {
class Subject extends Model {
static tableName = 'posts';
static scopes = {
isPublic() {
return this.where({
isPublic: true
});
}
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns an instance of `Query`', () => {
const result = Subject.unscope('isPublic');
expect(result).to.be.an.instanceof(Query);
});
});
describe('.hasScope()', () => {
class Subject extends Model {
static scopes = {
mostRecent() {
return this.order('createdAt', 'DESC');
}
};
}
it('returns true if a `Model` has a scope', () => {
const result = Subject.hasScope('mostRecent');
expect(result).to.be.true;
});
it('returns false if a `Model` does not have a scope', () => {
const result = Subject.hasScope('mostPopular');
expect(result).to.be.false;
});
});
describe('.isInstance()', () => {
class SubjectA extends Model {
static tableName = 'posts';
}
class SubjectB extends Model {
static tableName = 'posts';
}
before(async () => {
await Promise.all([
SubjectA.initialize(store, () => {
return store.connection(SubjectA.tableName);
}),
SubjectB.initialize(store, () => {
return store.connection(SubjectB.tableName);
})
]);
});
it('returns true if an object is an instance of the `Model`', () => {
const instance = new SubjectA();
const result = SubjectA.isInstance(instance);
expect(result).to.be.true;
});
it('returns false if an object is an instance of the `Model`', () => {
const instance = new SubjectA();
const result = SubjectB.isInstance(instance);
expect(result).to.be.false;
});
});
describe('.columnFor()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns the column data for an attribute if it exists', () => {
const result = Subject.columnFor('isPublic');
expect(result).to.be.an('object').and.have.all.keys([
'type',
'docName',
'nullable',
'maxLength',
'columnName',
'defaultValue'
]);
});
});
describe('.columnNameFor()', () => {
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns the column name for an attribute if it exists', () => {
const result = Subject.columnNameFor('isPublic');
expect(result).to.equal('is_public');
});
});
describe('.relationshipFor()', () => {
class Subject extends Model {
static tableName = 'posts';
static belongsTo = {
user: {
inverse: 'posts'
}
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns the data for a relationship if it exists', () => {
const result = Subject.relationshipFor('user');
expect(Reflect.ownKeys(result)).to.include.all.members([
'type',
'model',
'inverse',
'foreignKey'
]);
});
});
describe('.hooks', () => {
const assertCreateHook = (instance: Model, hookSpy) => {
expect(hookSpy.calledWith(instance)).to.be.true;
};
const assertSaveHook = async (instance: Model, hookSpy) => {
hookSpy.reset();
Reflect.set(instance, 'isPublic', true);
await instance.save();
expect(hookSpy.calledWith(instance)).to.be.true;
};
const assertUpdateHook = async (instance: Model, hookSpy) => {
hookSpy.reset();
await instance.update({
isPublic: true
});
expect(hookSpy.calledWith(instance)).to.be.true;
};
const assertDestroyHook = async (instance: Model, hookSpy) => {
await instance.destroy();
expect(hookSpy.calledWith(instance)).to.be.true;
};
describe('.afterCreate()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async afterCreate() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'afterCreate');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Hook (afterCreate)',
isPublic: false
});
});
after(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
});
describe('.afterDestroy()', () => {
let hookSpy;
let instance;
class Subject extends Model {
static tableName = 'posts';
static hooks = {
async afterDestroy() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'afterDestroy');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Hook (afterDestroy)',
isPublic: false
});
});
it('runs when #destroy is called', async () => {
await assertDestroyHook(instance, hookSpy);
});
});
describe('.afterSave()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async afterSave() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'afterSave');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (afterSave)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
describe('.afterUpdate()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async afterUpdate() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'afterUpdate');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (afterUpdate)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
describe('.afterValidation()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async afterValidation() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'afterValidation');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (afterValidation)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
describe('.beforeCreate()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async beforeCreate() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'beforeCreate');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Hook (beforeCreate)',
isPublic: false
});
});
after(async () => {
await instance.destroy();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
});
describe('.beforeDestroy()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async beforeDestroy(record) {
}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'beforeDestroy');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Hook (beforeDestroy)',
isPublic: false
});
});
after(async () => {
await instance.destroy();
});
it('runs when #destroy is called', async () => {
await assertDestroyHook(instance, hookSpy);
});
});
describe('.beforeSave()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async beforeSave() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'beforeSave');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (beforeSave)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
describe('.beforeUpdate()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
beforeUpdate(record) {
return Promise.resolve(record);
}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'beforeUpdate');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (beforeUpdate)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
describe('.beforeValidation()', () => {
let hookSpy;
let instance;
class Subject extends Model {
isPublic: boolean;
static tableName = 'posts';
static hooks = {
async beforeValidation() {}
};
}
before(async () => {
hookSpy = spy(Subject.hooks, 'beforeValidation');
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Hook (beforeValidation)',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
hookSpy.reset();
});
it('runs when .create() is called', () => {
assertCreateHook(instance, hookSpy);
});
it('runs when #save() is called', async () => {
await assertSaveHook(instance, hookSpy);
});
it('runs when #update() is called', async () => {
await assertUpdateHook(instance, hookSpy);
});
});
});
describe('.attributes', () => {
class Subject extends Model {
body: void | ?string;
title: string;
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
describe('#set()', () => {
const ogTitle = 'Test Attribute#set()';
let instance;
beforeEach(() => {
instance = new Subject({
title: ogTitle
});
});
it('updates the current value', () => {
const newVaue = 'It worked!';
instance.body = newVaue;
expect(instance).to.have.property('body', newVaue);
});
describe('- nullable', () => {
it('sets the current value to null when passed null', () => {
instance.body = null;
expect(instance).to.have.property('body', null);
});
it('sets the current value to null when passed undefined', () => {
instance.body = undefined;
expect(instance).to.have.property('body', null);
});
});
describe('- not nullable', () => {
it('does not update the current value when passed null', () => {
// $FlowIgnore
instance.title = null;
expect(instance).to.have.property('title', ogTitle);
});
it('does not update the current value when passed undefined', () => {
// $FlowIgnore
instance.title = undefined;
expect(instance).to.have.property('title', ogTitle);
});
});
});
});
describe('#save()', () => {
const instances = new Set();
let instance: Subject;
class Subject extends Model {
id: number;
user: Model;
title: string;
isPublic: boolean;
static tableName = 'posts';
static hasMany = {
comments: {
inverse: 'posts',
foreignKey: 'post_id'
}
};
static belongsTo = {
user: {
inverse: 'posts'
}
};
static validates = {
title: str => str.split(' ').length > 1
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Post',
isPublic: false
});
});
afterEach(() => {
return Subject.transaction(trx => (
Promise.all([
instance
.transacting(trx)
.destroy(),
...Array
.from(instances)
.map(record => (
record
.transacting(trx)
.destroy()
.then(() => instances.delete(record))
))
])
));
});
it('can persist dirty attributes', async () => {
instance.isPublic = true;
await instance.save();
expect(instance).to.have.property('isPublic', true);
const result = await Subject.find(instance.id);
expect(result).to.have.property('isPublic', true);
});
it('can persist dirty relationships', async () => {
const userInstance = await User.create({
name: 'Test User',
email: 'test-user@postlight.com',
password: 'test12345678'
});
instances.add(userInstance);
instance.user = userInstance;
await instance.save();
const {
rawColumnData: {
user,
userId
}
} = await Subject
.find(instance.id)
.include('user');
expect(user).to.be.an('object');
expect(user).to.have.property('id', userId);
expect(user).to.have.property('name', 'Test User');
expect(user).to.have.property('email', 'test-user@postlight.com');
});
it('fails if a validation is not met', async () => {
instance.title = 'Test';
await instance.save().catch(err => {
expect(err).to.be.an.instanceof(ValidationError);
});
expect(instance).to.have.property('title', 'Test');
const result = await Subject.find(instance.id);
expect(result).to.have.property('title', 'Test Post');
});
});
describe('#update()', () => {
let instance: Subject;
class Subject extends Model {
id: number;
title: string;
isPublic: boolean;
static tableName = 'posts';
static hasOne = {
image: {
inverse: 'post'
}
};
static hasMany = {
comments: {
inverse: 'post'
}
};
static belongsTo = {
user: {
inverse: 'posts'
}
};
static validates = {
title: str => str.split(' ').length > 1
};
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
beforeEach(async () => {
instance = await Subject.create({
title: 'Test Post',
isPublic: false
});
});
afterEach(async () => {
await instance.destroy();
});
it('can set and persist attributes and relationships', async () => {
const body = 'Lots of content...';
const user = new User({
id: 1
});
const image = new Image({
id: 1
});
const comments = [
new Comment({
id: 1
}),
new Comment({
id: 2
}),
new Comment({
id: 3
})
];
await instance.update({
body,
user,
image,
comments,
isPublic: true
});
expect(instance).to.have.property('body', body);
expect(instance).to.have.property('isPublic', true);
// $FlowIgnore
expect(await instance.user)
.to.have.property('id', user.getPrimaryKey());
// $FlowIgnore
expect(await instance.image)
.to.have.property('id', image.getPrimaryKey());
// $FlowIgnore
expect(await instance.comments)
.to.be.an('array')
.with.lengthOf(3);
const result = await Subject
.find(instance.id)
.include('user', 'image', 'comments');
expect(result).to.have.property('body', body);
expect(result).to.have.property('isPublic', true);
expect(await result.user)
.to.have.property('id', user.getPrimaryKey());
expect(await result.image)
.to.have.property('id', image.getPrimaryKey());
expect(await result.comments)
.to.be.an('array')
.with.lengthOf(3);
});
it('fails if a validation is not met', async () => {
await instance
.update({
title: 'Test',
isPublic: true
})
.catch(err => {
expect(err).to.be.an.instanceof(ValidationError);
});
expect(instance).to.have.property('title', 'Test');
expect(instance).to.have.property('isPublic', true);
const result = await Subject.find(instance.id);
expect(result).to.have.property('title', 'Test Post');
expect(result).to.have.property('isPublic', false);
});
});
describe('#destroy()', () => {
let instance: Subject;
class Subject extends Model {
id: number;
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Post'
});
});
after(async () => {
await instance.destroy();
});
it('removes the record from the database', async () => {
await instance.destroy();
await Subject.find(instance.id).catch(err => {
expect(err).to.be.an.instanceof(RecordNotFoundError);
});
});
});
describe('#reload', () => {
let instance: Subject;
class Subject extends Model {
title: string;
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
body: 'Lots of content...',
title: 'Test Post',
isPublic: true
});
});
after(async () => {
await instance.destroy();
});
it('reverts attributes to the last known persisted changes', async () => {
const { title: prevTitle } = instance;
const nextTitle = 'Testing #reload()';
instance.title = nextTitle;
const ref = await instance.reload();
expect(ref).to.not.equal(instance);
expect(ref).to.have.property('title', prevTitle);
expect(instance).to.have.property('title', nextTitle);
});
it('resolves with itself if the instance is new', async () => {
const newInstance = new Subject();
const nextTitle = 'Testing #reload()';
newInstance.title = nextTitle;
const ref = await newInstance.reload();
expect(ref).to.equal(newInstance);
expect(ref).to.have.property('title', nextTitle);
});
});
describe('#rollback', () => {
let instance: Subject;
class Subject extends Model {
title: string;
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
body: 'Lots of content...',
title: 'Test Post',
isPublic: true
});
});
after(async () => {
await instance.destroy();
});
it('reverts attributes to the last known persisted changes', () => {
const { title: prevTitle } = instance;
const nextTitle = 'Testing #rollback()';
instance.title = nextTitle;
expect(instance).to.have.property('title', nextTitle);
const ref = instance.rollback();
expect(ref).to.equal(instance);
expect(instance).to.have.property('title', prevTitle);
});
});
describe('#getAttributes()', () => {
let instance: Subject;
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
body: 'Lots of content...',
title: 'Test Post',
isPublic: true
});
});
after(async () => {
await instance.destroy();
});
it('returns a pojo containing the requested attributes', () => {
const result = instance.getAttributes('body', 'title');
expect(result).to.deep.equal({
body: 'Lots of content...',
title: 'Test Post'
});
});
});
describe('#getPrimaryKey()', () => {
let instance: Subject;
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
instance = await Subject.create({
title: 'Test Post'
});
});
after(async () => {
await instance.destroy();
});
it('returns the value of `instance[Model.primaryKey]`', () => {
const result = instance.getPrimaryKey();
expect(result).to.be.a('number');
});
});
describe('get #persisted()', () => {
let instance;
class Subject extends Model {
static tableName = 'posts';
}
before(async () => {
await Subject.initialize(store, () => {
return store.connection(Subject.tableName);
});
});
it('returns true for a record returned from querying', async () => {
const record = await Subject.first();
expect(record).to.have.property('persisted', true);
});
it('returns false if a record has been modified', async () => {
const record = await Subject.first();
record.title = 'Modified Title';
expect(record).to.have.property('persisted', false);
});
it('returns false if the record is new', () => {
const record = new Subject();
expect(record).to.have.property('persisted', false);
});
});
});
});