Merge pull request #240 from anatoliychakkaev/master

Docs and all pieces matters
This commit is contained in:
Anatoliy Chakkaev 2013-03-26 17:54:07 -07:00
commit 121b13511d
13 changed files with 941 additions and 355 deletions

View File

@ -28,7 +28,7 @@ docs/man/%.3: docs/%.md scripts/doc.sh
docs/html/%.3.html: docs/%.md scripts/doc.sh docs/footer.html
scripts/doc.sh $< $@
docs/html/index.html: docs/jugglingdb.md scripts/doc.sh
docs/html/index.html: docs/jugglingdb.md scripts/doc.sh docs/footer.html
scripts/doc.sh $< $@
man: $(MAN_DOCS)

View File

@ -1,15 +1,21 @@
<script>
var filename = location.href.match(/([^\/]+)?\.3.html(#.*?)?$/)[1];
var filename = location.href.match(/([^\/]+)?\.3.html(#.*?)?$/);
if (!filename) {
filename = [null, 'jugglingdb'];
}
var div = document.createElement('div');
div.innerHTML = 'Found a typo? ' +
linkTo('View', 'blob') + ' and ' +
linkTo('edit', 'edit') + ' this file online at GitHub.';
linkTo('View', 'blob') +
' and ' +
'edit' +
//linkTo('edit', 'edit') +
' this file online at GitHub.';
document.getElementById('man').appendChild(div);
function linkTo(text, dir) {
return '<a href="https://github.com/1602/jugglingdb/' +
dir + '/master/docs/' + filename + '.md">' + text + '</a>';
dir + '/master/docs/' + filename[1] + '.md">' + text + '</a>';
}
addLink('h3', '&para;');

View File

@ -1,2 +1,135 @@
jugglingdb-hooks(3) - Hooks and object lifecycle.
==================
===================
## DESCRIPTION
Hook is a class method called on object when some event happens. List of events:
* `initialize`:
Called after `new Model` called.
* `create`:
Called before and after create.
* `update`:
Called before and after save (except create).
* `save`:
Called before and after save (including both create and update).
* `validate`:
Called before and after validations.
* `destroy`:
Called before and after destroy on instance.
Each hook except `initialize` accepts callback as first argument. This callback
should be called when hook done. All hooks called on object instance, but it's
not recommended to use `this` for updating in all hooks where data argument
available (second argument for all data-related before-hooks: save, update,
create).
## INITIALIZE
Initialize hook called when new object created after all setters and default
being applied.
Model.afterInitialize = function() {
this.property = 'some value;
console.log('afterInitialize called');
};
new Model; // afterInitialize called
## CREATE
Create hooks called when object created.
The `beforeCreate` hook accepts `data` as a second argument.
Model.beforeCreate = function(next, data) {
// use data argument to update object
data.createdAt = new Date();
console.log('before');
next();
};
Model.afterCreate = function(next) {
this.notifySocialNetworks();
this.sendEmailNotifications();
console.log('after');
next();
};
Model.create({foo: 'bar'}, function(err, model) {
console.log('callback');
});
Example output will be:
before
after
callback
## UPDATE
Update hooks called on each save except create.
The `beforeUpdate` hook accepts data as second argument.
Data argument only containing actual data for update, not full object data.
Model.beforeUpdate = function(next, data) {
// use data argument to update object
// in update hook data argumen only contains data for update (not
// full object)
data.updatedAt = new Date();
console.log('before');
next();
};
Model.afterUpdate = function(next) {
this.scheduleFulltextIndexUpdate();
console.log('after');
next();
};
model.updateAttributes({foo: 'bar'}, function(err, model) {
console.log('callback');
});
Example output will be:
before
after
callback
## SAVE
Save hooks called on each save, both update and create.
The `beforeSave` hook accepts `data` as a second argument.
For `beforeSave` hook `data` argument is the same as `this`.
Model.beforeSave = function(next, data) {
if ('string' !== typeof data.tags) {
data.tags = JSON.stringify(data.tags);
}
next();
};
Model.afterSave = function(next) {
next();
};
## DESTROY
Destroy hooks called when `model.destroy()` called. Please note that
`destroyAll` method doesn't call destroy hooks.
## VALIDATE
Validate hooks callen before and after validation and should be used for data
modification and not for validation. Use custom validation described in
jugglingdb-validations(3) man section.
## SEE ALSO
jugglingdb-model(3)
jugglingdb-validations(3)

View File

@ -109,9 +109,70 @@ count filtered set of records. Callback called with error and count arguments.
console.log(count); // count of approved users stored in database
});
## ESSENTIALS
## RELATIONS
### Default values
### hasMany
Define all necessary stuff for "one to many" relation:
* foreign key in "many" model
* named scope in "one" model
Example:
var Book = db.define('Book');
var Chapter = db.define('Chapters');
// syntax 1 (old):
Book.hasMany(Chapter);
// syntax 2 (new):
Book.hasMany('chapters');
Syntax 1 and 2 does same things in different ways: adds `chapters` method to
`Book.prototype` and add `bookId` property to `Chapter` model. Foreign key name
(`bookId`) could be specified manually using second param:
Book.hasMany('chapters', {foreignKey: `chapter_id`});
When using syntax 2 jugglingdb looking for model with singularized name:
'chapters' => 'chapter' => 'Chapter'
But it's possible to specify model manually using second param:
Book.hasMany('stories', {model: Chapter});
Syntax 1 allows to override scope name using `as` property of second param:
Book.hasMany(Chapter, {as: 'stories'});
**Scope methods** created on BaseClass by hasMany allows to build, create and
query instances of other class. For example:
Book.create(function(err, book) {
// using 'chapters' scope for build:
var c = book.chapters.build({name: 'Chapter 1'});
// same as:
c = new Chapter({name: 'Chapter 1', bookId: book.id});
// using 'chapters' scope for create:
book.chapters.create();
// same as:
Chapter.create({bookId: book.id});
// using scope for querying:
book.chapters(function() {/* all chapters with bookId = book.id */ });
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
});
});
### belongsTo
TODO: document
### hasAndBelongsToMany
TODO: implement and document
## SEE ALSO

View File

@ -70,11 +70,6 @@ Memory.prototype.all = function all(model, filter, callback) {
if (filter) {
// do we need some filtration?
if (filter.where) {
nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes;
}
// do we need some sorting?
if (filter.order) {
var props = this._models[model].properties;
@ -87,12 +82,18 @@ Memory.prototype.all = function all(model, filter, callback) {
var m = key.match(/\s+(A|DE)SC$/i);
if (m) {
key = key.replace(/\s+(A|DE)SC/i, '');
if (m[1] === 'DE') reverse = -1;
if (m[1].toLowerCase() === 'de') reverse = -1;
}
orders[i] = {"key": key, "reverse": reverse};
});
nodes = nodes.sort(sorting.bind(orders));
}
// do we need some filtration?
if (filter.where) {
nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes;
}
}
process.nextTick(function () {

View File

@ -512,6 +512,12 @@ AbstractClass.include = function (objects, include, cb) {
}
var relation = relations[relationName];
if (!relation) {
return function() {
cb(new Error('Relation "' + relationName + '" is not defined for ' + self.modelName + ' model'));
}
}
var req = {'where': {}};
if (!keyVals[relation.keyFrom]) {
@ -954,12 +960,26 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
* This optional parameter default value is false, so the related object will be loaded from cache if available.
*/
AbstractClass.belongsTo = function (anotherClass, params) {
var methodName = params.as;
var fk = params.foreignKey;
params = params || {};
if ('string' === typeof anotherClass) {
params.as = anotherClass;
if (params.model) {
anotherClass = params.model;
} else {
var anotherClassName = anotherClass.toLowerCase();
for(var name in this.schema.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.schema.models[name];
}
}
}
}
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
var fk = params.foreignKey || methodName + 'Id';
this.relations[params['as']] = {
type: 'belongsTo',
keyFrom: params['foreignKey'],
keyFrom: fk,
keyTo: 'id',
modelTo: anotherClass,
multiple: false

249
test/basic-querying.test.js Normal file
View File

@ -0,0 +1,249 @@
var db, User, should = require('should');
describe('basic-querying', function() {
before(function(done) {
db = getSchema();
User = db.define('User', {
name: String,
email: {type: String, index: true},
role: {type: String, index: true},
order: {type: Number, index: true}
});
db.automigrate(done);
});
describe('find', function() {
before(function(done) {
User.destroyAll(done);
});
it('should query by id: not found', function(done) {
User.find(1, function(err, u) {
should.not.exist(u);
should.not.exist(err);
done();
});
});
it('should query by id: found', function(done) {
User.create(function(err, u) {
should.exist(u.id);
User.find(u.id, function(err, u) {
should.exist(u);
should.not.exist(err);
u.should.be.an.instanceOf(User);
done();
});
});
});
});
describe('all', function() {
before(seed);
it('should query collection', function(done) {
User.all(function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(6);
done();
});
});
it('should query filtered collection', function(done) {
User.all({where: {role: 'lead'}}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.should.have.lengthOf(2);
done();
});
});
it('should query collection sorted by numeric field', function(done) {
User.all({order: 'order'}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.forEach(function(u, i) {
u.order.should.eql(i + 1);
});
done();
});
});
it('should query collection desc sorted by numeric field', function(done) {
User.all({order: 'order DESC'}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.forEach(function(u, i) {
u.order.should.eql(users.length - i);
});
done();
});
});
it('should query collection sorted by string field', function(done) {
User.all({order: 'name'}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.shift().name.should.equal('George Harrison');
users.shift().name.should.equal('John Lennon');
users.pop().name.should.equal('Stuart Sutcliffe');
done();
});
});
it('should query collection desc sorted by string field', function(done) {
User.all({order: 'name DESC'}, function(err, users) {
should.exists(users);
should.not.exists(err);
users.pop().name.should.equal('George Harrison');
users.pop().name.should.equal('John Lennon');
users.shift().name.should.equal('Stuart Sutcliffe');
done();
});
});
});
describe('count', function() {
before(seed);
it('should query total count', function(done) {
User.count(function(err, n) {
should.not.exist(err);
should.exist(n);
n.should.equal(6);
done();
});
});
it('should query filtered count', function(done) {
User.count({role: 'lead'}, function(err, n) {
should.not.exist(err);
should.exist(n);
n.should.equal(2);
done();
});
});
});
describe('findOne', function() {
before(seed);
it('should find first record (default sort by id)', function(done) {
User.all({sort: 'id'}, function(err, users) {
User.findOne(function(e, u) {
should.not.exist(e);
should.exist(u);
u.id.should.equal(users[0].id);
done();
});
});
});
it('should find first record', function(done) {
User.findOne({order: 'order'}, function(e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(1);
u.name.should.equal('Paul McCartney');
done();
});
});
it('should find last record', function(done) {
User.findOne({order: 'order DESC'}, function(e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(6);
u.name.should.equal('Ringo Starr');
done();
});
});
it('should find last record in filtered set', function(done) {
User.findOne({
where: {role: 'lead'},
order: 'order DESC'
}, function(e, u) {
should.not.exist(e);
should.exist(u);
u.order.should.equal(2);
u.name.should.equal('John Lennon');
done();
});
});
});
describe('exists', function() {
before(seed);
it('should check whether record exist', function(done) {
User.findOne(function(e, u) {
User.exists(u.id, function(err, exists) {
should.not.exist(err);
should.exist(exists);
exists.should.be.ok;
done();
});
});
});
it('should check whether record not exist', function(done) {
User.destroyAll(function() {
User.exists(42, function(err, exists) {
should.not.exist(err);
exists.should.not.be.ok;
done();
});
});
});
});
});
function seed(done) {
var count = 0;
var beatles = [
{
name: 'John Lennon',
mail: 'john@b3atl3s.co.uk',
role: 'lead',
order: 2
}, {
name: 'Paul McCartney',
mail: 'paul@b3atl3s.co.uk',
role: 'lead',
order: 1
},
{name: 'George Harrison', order: 5},
{name: 'Ringo Starr', order: 6},
{name: 'Pete Best', order: 4},
{name: 'Stuart Sutcliffe', order: 3}
];
User.destroyAll(function() {
beatles.forEach(function(beatle) {
User.create(beatle, ok);
});
});
function ok() {
if (++count === beatles.length) {
done();
}
}
}

2
test/common.batch.js Normal file
View File

@ -0,0 +1,2 @@
require('./basic-querying.test.js');
require('./hooks.test.js');

View File

@ -209,42 +209,6 @@ function testOrm(schema) {
test.done();
});
it('should be exported to JSON', function (test) {
var outString = '{"title":"hello, json","date":1,"published":false,"likes":[],"related":[],"id":1}'
if (schema.name === 'nano')
outString = '{"title":"hello, json","subject":null,"content":null,"date":1,"published":false,"likes":[],"related":[],"_rev":null,"id":1,"userId":null}'
test.equal(JSON.stringify(new Post({id: 1, title: 'hello, json', date: 1})),outString);
test.done();
});
it('should create object', function (test) {
Post.create(function (err, post) {
if (err) throw err;
test.ok(post.id, 'Id present');
test.ok(!post.title, 'Title is blank');
Post.exists(post.id, function (err, exists) {
if (err) throw err;
test.ok(exists);
test.done();
});
});
});
it('should create object without callback', function (test) {
var uniqueTitle = 'Unique title ' + Date.now();
Post.create({title: uniqueTitle});
setTimeout(delayedCallback, 100);
function delayedCallback() {
Post.all({where: {title: uniqueTitle}}, function (err, posts) {
test.equal(posts.length, 1);
test.done();
});
}
});
it('should save object', function (test) {
var title = 'Initial title', title2 = 'Hello world',
date = new Date;
@ -420,53 +384,6 @@ function testOrm(schema) {
});
});
it('should fetch count of records in collection', function (test) {
Post.count(function (err, count) {
console.log(countOfposts, count);
test.equal(countOfposts, count, 'unfiltered count');
Post.count({title: 'title'}, function (err, count) {
console.log(countOfpostsFiltered, count, 'filtered count');
test.equal(countOfpostsFiltered, count, 'filtered count');
test.done();
});
});
});
it('should find filtered set of records', function (test) {
var wait = 1;
// exact match with string
Post.all({where: {title: 'New title'}}, function (err, res) {
var pass = true;
res.forEach(function (r) {
if (r.title != 'New title') pass = false;
});
test.ok(res.length > 0, 'Exact match with string returns dataset');
test.ok(pass, 'Exact match with string');
done();
});
// matching null
// Post.all({where: {title: null}}, function (err, res) {
// var pass = true;
// res.forEach(function (r) {
// if (r.title != null) pass = false;
// });
// test.ok(res.length > 0, 'Matching null returns dataset');
// test.ok(pass, 'Matching null');
// done();
// });
function done() {
if (--wait === 0) {
test.done();
}
}
});
it('should find records filtered with multiple attributes', function (test) {
var d = new Date;
Post.create({title: 'title', content: 'content', published: true, date: d}, function (err, post) {
@ -661,252 +578,6 @@ function testOrm(schema) {
};
});
if (
schema.name === 'mysql' ||
schema.name === 'sqlite3' ||
schema.name === 'postgres'
)
it('should handle include function', function (test) {
var createdUsers = [];
var createdPassports = [];
var createdPosts = [];
var context = null;
createUsers();
function createUsers() {
clearAndCreate(
User,
[
{name: 'User A', age: 21},
{name: 'User B', age: 22},
{name: 'User C', age: 23},
{name: 'User D', age: 24},
{name: 'User E', age: 25}
],
function(items) {
createdUsers = items;
createPassports();
}
);
}
function createPassports() {
clearAndCreate(
Passport,
[
{number: '1', ownerId: createdUsers[0].id},
{number: '2', ownerId: createdUsers[1].id},
{number: '3'}
],
function(items) {
createdPassports = items;
createPosts();
}
);
}
function createPosts() {
clearAndCreate(
Post,
[
{title: 'Post A', userId: createdUsers[0].id},
{title: 'Post B', userId: createdUsers[0].id},
{title: 'Post C', userId: createdUsers[0].id},
{title: 'Post D', userId: createdUsers[1].id},
{title: 'Post E'}
],
function(items) {
createdPosts = items;
makeTests();
}
);
}
function makeTests() {
var unitTests = [
function() {
context = ' (belongsTo simple string from passports to users)';
Passport.all({include: 'owner'}, testPassportsUser);
},
function() {
context = ' (belongsTo simple string from posts to users)';
Post.all({include: 'author'}, testPostsUser);
},
function() {
context = ' (belongsTo simple array)';
Passport.all({include: ['owner']}, testPassportsUser);
},
function() {
context = ' (hasMany simple string from users to posts)';
User.all({include: 'posts'}, testUsersPosts);
},
function() {
context = ' (hasMany simple string from users to passports)';
User.all({include: 'passports'}, testUsersPassports);
},
function() {
context = ' (hasMany simple array)';
User.all({include: ['posts']}, testUsersPosts);
},
function() {
context = ' (Passports - User - Posts in object)';
Passport.all({include: {'owner': 'posts'}}, testPassportsUserPosts);
},
function() {
context = ' (Passports - User - Posts in array)';
Passport.all({include: [{'owner': 'posts'}]}, testPassportsUserPosts);
},
function() {
context = ' (Passports - User - Posts - User)';
Passport.all({include: {'owner': {'posts': 'author'}}}, testPassportsUserPosts);
},
function() {
context = ' (User - Posts AND Passports)';
User.all({include: ['posts', 'passports']}, testUsersPostsAndPassports);
}
];
function testPassportsUser(err, passports, callback) {
testBelongsTo(passports, 'owner', callback);
}
function testPostsUser(err, posts, callback) {
testBelongsTo(posts, 'author', callback);
}
function testBelongsTo(items, relationName, callback) {
if (typeof callback === 'undefined') {
callback = nextUnitTest;
}
var nbInitialRequests = nbSchemaRequests;
var nbItemsRemaining = items.length;
for (var i = 0; i < items.length; i++) {
testItem(items[i]);
}
function testItem(item) {
var relation = item.constructor.relations[relationName];
var modelNameFrom = item.constructor.modelName;
var modelNameTo = relation.modelTo.modelName;
item[relationName](function(err, relatedItem) {
if (relatedItem !== null) {
test.equal(relatedItem[relation.keyTo], item[relation.keyFrom], modelNameTo + '\'s instance match ' + modelNameFrom + '\'s instance' + context);
} else {
test.ok(item[relation.keyFrom] == null, 'User match passport even when user is null.' + context);
}
nbItemsRemaining--;
if (nbItemsRemaining == 0) {
requestsAreCounted && test.equal(nbSchemaRequests, nbInitialRequests, 'No more request have been executed for loading ' + relationName + ' relation' + context)
callback();
}
});
}
}
function testUsersPosts(err, users, expectedUserNumber, callback) {
if (typeof expectedUserNumber === 'undefined') {
expectedUserNumber = 5;
}
test.equal(users.length, expectedUserNumber, 'Exactly ' + expectedUserNumber + ' users returned by query' + context);
testHasMany(users, 'posts', callback);
}
function testUsersPassports(err, users, callback) {
testHasMany(users, 'passports', callback);
}
function testHasMany(items, relationName, callback) {
if (typeof callback === 'undefined') {
callback = nextUnitTest;
}
var nbInitialRequests = nbSchemaRequests;
var nbItemRemaining = items.length;
for (var i = 0; i < items.length; i++) {
testItem(items[i]);
}
function testItem(item) {
var relation = item.constructor.relations[relationName];
var modelNameFrom = item.constructor.modelName;
var modelNameTo = relation.modelTo.modelName;
item[relationName](function(err, relatedItems) {
for (var j = 0; j < relatedItems.length; j++) {
test.equal(relatedItems[j][relation.keyTo], item[relation.keyFrom], modelNameTo + '\'s instances match ' + modelNameFrom + '\'s instance' + context);
}
nbItemRemaining--;
if (nbItemRemaining == 0) {
requestsAreCounted && test.equal(nbSchemaRequests, nbInitialRequests, 'No more request have been executed for loading ' + relationName + ' relation' + context)
callback();
}
});
}
}
function testPassportsUserPosts(err, passports) {
testPassportsUser(err, passports, function() {
var nbPassportsRemaining = passports.length;
for (var i = 0; i < passports.length; i++) {
if (passports[i].ownerId !== null) {
passports[i].owner(function(err, user) {
testUsersPosts(null, [user], 1, function() {
nextPassport();
});
});
} else {
nextPassport();
}
}
function nextPassport() {
nbPassportsRemaining--
if (nbPassportsRemaining == 0) {
nextUnitTest();
}
}
});
}
function testUsersPostsAndPassports(err, users) {
testUsersPosts(err, users, 5, function() {
testUsersPassports(err, users, function() {
nextUnitTest();
});
});
}
var testNum = 0;
function nextUnitTest() {
if (testNum >= unitTests.length) {
test.done();
return;
}
unitTests[testNum]();
testNum++;
}
nextUnitTest();
}
});
it('should destroy all records', function (test) {
Post.destroyAll(function (err) {
if (err) {
console.log('Error in destroyAll');
console.log(err);
throw err;
}
Post.all(function (err, posts) {
test.equal(posts.length, 0);
Post.count(function (err, count) {
test.equal(count, 0);
test.done();
});
});
});
});
it('should return type of property', function (test) {
test.equal(Post.whatTypeName('title'), 'String');
test.equal(Post.whatTypeName('content'), 'Text');

View File

@ -8,15 +8,17 @@ var j = require('../'),
describe('hooks', function() {
before(function() {
before(function(done) {
db = getSchema();
User = db.define('User', {
email: String,
email: {type: String, index: true},
name: String,
password: String,
state: String
});
db.automigrate(done);
});
describe('initialize', function() {
@ -110,6 +112,26 @@ describe('hooks', function() {
user.save();
});
});
it('should save actual modifications to database', function(done) {
User.beforeSave = function(next, data) {
data.password = 'hash';
next();
};
User.destroyAll(function() {
User.create({
email: 'james.bond@example.com',
password: 'secret'
}, function() {
User.findOne({
where: {email: 'james.bond@example.com'}
}, function(err, jb) {
jb.password.should.equal('hash');
done();
});
});
});
});
});
describe('update', function() {

204
test/include.test.js Normal file
View File

@ -0,0 +1,204 @@
var db, User, Post, Passport, City, Street, Building, should = require('should');
var nbSchemaRequests = 0;
describe('include', function() {
before(setup);
it('should fetch belongsTo relation', function(done) {
Passport.all({include: 'owner'}, function (err, passports) {
passports.length.should.be.ok;
passports.forEach(function(p) {
p.__cachedRelations.should.have.property('owner');
if (p.ownerId === null) {
should.not.exist(p.__cachedRelations.owner);
} else {
p.__cachedRelations.owner.id.should.equal(p.ownerId);
}
});
done();
});
});
it('should fetch hasMany relation', function(done) {
User.all({include: 'posts'}, function (err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
users.forEach(function(u) {
u.__cachedRelations.should.have.property('posts');
u.__cachedRelations.posts.forEach(function(p) {
p.userId.should.equal(u.id);
});
});
done();
});
});
it('should fetch Passport - Owner - Posts', function(done) {
Passport.all({include: {owner: 'posts'}}, function(err, passports) {
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
passports.forEach(function(p) {
p.__cachedRelations.should.have.property('owner');
var user = p.__cachedRelations.owner;
if (p.ownerId === null) {
should.not.exist(user);
} else {
user.id.should.equal(p.ownerId);
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.posts.forEach(function(pp) {
pp.userId.should.equal(user.id);
});
}
});
done();
});
});
it('should fetch Passports - User - Posts - User', function(done) {
Passport.all({
include: {owner: {posts: 'author'}}
}, function(err, passports) {
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
passports.forEach(function(p) {
p.__cachedRelations.should.have.property('owner');
var user = p.__cachedRelations.owner;
if (p.ownerId === null) {
should.not.exist(user);
} else {
user.id.should.equal(p.ownerId);
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.posts.forEach(function(pp) {
pp.userId.should.equal(user.id);
pp.__cachedRelations.should.have.property('author');
var author = pp.__cachedRelations.author;
author.id.should.equal(user.id);
});
}
});
done();
});
});
it('should fetch User - Posts AND Passports', function(done) {
User.all({include: ['posts', 'passports']}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
users.forEach(function(user) {
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
user.__cachedRelations.posts.forEach(function(p) {
p.userId.should.equal(user.id);
});
user.__cachedRelations.passports.forEach(function(pp) {
pp.ownerId.should.equal(user.id);
});
});
done();
});
});
});
function setup(done) {
db = getSchema();
City = db.define('City');
Street = db.define('Street');
Building = db.define('Building');
User = db.define('User', {
name: String,
age: Number
});
Passport = db.define('Passport', {
number: String
});
Post = db.define('Post', {
title: String
});
Passport.belongsTo('owner', {model: User});
User.hasMany('passports', {foreignKey: 'ownerId'});
User.hasMany('posts', {foreignKey: 'userId'});
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
db.automigrate(function() {
var createdUsers = [];
var createdPassports = [];
var createdPosts = [];
createUsers();
function createUsers() {
clearAndCreate(
User,
[
{name: 'User A', age: 21},
{name: 'User B', age: 22},
{name: 'User C', age: 23},
{name: 'User D', age: 24},
{name: 'User E', age: 25}
],
function(items) {
createdUsers = items;
createPassports();
}
);
}
function createPassports() {
clearAndCreate(
Passport,
[
{number: '1', ownerId: createdUsers[0].id},
{number: '2', ownerId: createdUsers[1].id},
{number: '3'}
],
function(items) {
createdPassports = items;
createPosts();
}
);
}
function createPosts() {
clearAndCreate(
Post,
[
{title: 'Post A', userId: createdUsers[0].id},
{title: 'Post B', userId: createdUsers[0].id},
{title: 'Post C', userId: createdUsers[0].id},
{title: 'Post D', userId: createdUsers[1].id},
{title: 'Post E'}
],
function(items) {
createdPosts = items;
done();
}
);
}
});
}
function clearAndCreate(model, data, callback) {
var createdItems = [];
model.destroyAll(function () {
nextItem(null, null);
});
var itemIndex = 0;
function nextItem(err, lastItem) {
if (lastItem !== null) {
createdItems.push(lastItem);
}
if (itemIndex >= data.length) {
callback(createdItems);
return;
}
model.create(data[itemIndex], nextItem);
itemIndex++;
}
}

137
test/manipulation.test.js Normal file
View File

@ -0,0 +1,137 @@
var db, Person, should = require('should');
describe('manipulation', function() {
before(function(done) {
db = getSchema();
Person = db.define('Person', {
name: String,
gender: String,
married: Boolean,
age: {type: Number, index: true},
dob: Date,
createdAt: {type: Number, default: Date.now}
});
db.automigrate(done);
});
describe('create', function() {
before(function(done) {
Person.destroyAll(done);
});
it('should create instance', function(done) {
Person.create({name: 'Anatoliy'}, function(err, p) {
p.name.should.equal('Anatoliy');
should.not.exist(err);
should.exist(p);
Person.find(p.id, function(err, person) {
person.id.should.equal(p.id);
person.name.should.equal('Anatoliy');
done();
});
});
});
it('should work when called without callback', function(done) {
Person.afterCreate = function(next) {
this.should.be.an.instanceOf(Person);
this.name.should.equal('Nickolay');
should.exist(this.id);
Person.afterCreate = null;
next();
setTimeout(done, 10);
};
Person.create({name: 'Nickolay'});
});
it('should create instance with blank data', function(done) {
Person.create(function(err, p) {
should.not.exist(err);
should.exist(p);
should.not.exists(p.name);
Person.find(p.id, function(err, person) {
person.id.should.equal(p.id);
should.not.exists(person.name);
done();
});
});
});
it('should work when called with no data and callback', function(done) {
Person.afterCreate = function(next) {
this.should.be.an.instanceOf(Person);
should.not.exist(this.name);
should.exist(this.id);
Person.afterCreate = null;
next();
setTimeout(done, 10);
};
Person.create();
});
});
describe('save', function() {
it('should save new object');
it('should save existing object');
it('should save invalid object (skipping validation)');
it('should save throw error on validation');
});
describe('destroy', function() {
it('should destroy record', function(done) {
Person.create(function(err, p){
p.destroy(function(err) {
should.not.exist(err);
Person.exists(p.id, function(err, ex) {
ex.should.not.be.ok;
done();
});
});
});
});
it('should destroy all records', function (done) {
Person.destroyAll(function (err) {
should.not.exist(err);
Person.all(function (err, posts) {
posts.should.have.lengthOf(0);
Person.count(function (err, count) {
count.should.eql(0);
done();
});
});
});
});
// TODO: implement destroy with filtered set
it('should destroy filtered set of records');
});
describe('initialize', function() {
it('should initialize object properly', function() {
var hw = 'Hello word',
now = Date.now(),
person = new Person({name: hw});
person.name.should.equal(hw);
person.propertyChanged('name').should.be.false;
person.name = 'Goodbye, Lenin';
person.name_was.should.equal(hw);
person.propertyChanged('name').should.be.true;
(person.createdAt >= now).should.be.true;
person.isNewRecord().should.be.true;
});
it('should work when constructor called as function', function() {
var p = Person({name: 'John Resig'});
p.should.be.an.instanceOf(Person);
p.name.should.equal('John Resig');
});
});
});

View File

@ -1,12 +1,22 @@
var db, Book, Chapter, Author, Reader;
var db, Book, Chapter, Author, Reader, should = require('should');
describe('relations', function() {
before(function() {
before(function(done) {
db = getSchema();
Book = db.define('Book', {name: String});
Chapter = db.define('Chapter', {name: String});
Chapter = db.define('Chapter', {name: {type: String, index: true}});
Author = db.define('Author', {name: String});
Reader = db.define('Reader', {name: String});
db.automigrate(function() {
Book.destroyAll(function() {
Chapter.destroyAll(function() {
Author.destroyAll(function() {
Reader.destroyAll(done);
});
});
});
});
});
after(function() {
@ -14,7 +24,7 @@ describe('relations', function() {
});
describe('hasMany', function() {
it('can be declared in different ways', function() {
it('can be declared in different ways', function(done) {
Book.hasMany(Chapter);
Book.hasMany(Reader, {as: 'users'});
Book.hasMany(Author, {foreignKey: 'projectId'});
@ -24,17 +34,87 @@ describe('relations', function() {
b.authors.should.be.an.instanceOf(Function);
Object.keys((new Chapter).toObject()).should.include('bookId');
Object.keys((new Author).toObject()).should.include('projectId');
db.automigrate(done);
});
it('can be declared in short form', function() {
it('can be declared in short form', function(done) {
Author.hasMany('readers');
(new Author).readers.should.be.an.instanceOf(Function);
Object.keys((new Reader).toObject()).should.include('authorId');
db.automigrate(done);
});
it('should build record on scope', function(done) {
Book.create(function(err, book) {
var c = book.chapters.build();
c.bookId.should.equal(book.id);
c.save(done);
});
});
it('should create record on scope', function(done) {
Book.create(function(err, book) {
book.chapters.create(function(err, c) {
should.not.exist(err);
should.exist(c);
c.bookId.should.equal(book.id);
done();
});
});
});
it('should fetch all scoped instances', function(done) {
Book.create(function(err, book) {
book.chapters.create({name: 'a'}, function() {
book.chapters.create({name: 'z'}, function() {
book.chapters.create({name: 'c'}, function() {
fetch(book);
});
});
});
});
function fetch(book) {
book.chapters(function(err, ch) {
should.not.exist(err);
should.exist(ch);
ch.should.have.lengthOf(3);
book.chapters({order: 'name DESC'}, function(e, c) {
should.not.exist(e);
should.exist(c);
c.shift().name.should.equal('z');
c.pop().name.should.equal('a');
done();
});
});
}
});
});
describe('belongsTo', function() {
it('can be declared in different ways');
var List, Item, Fear, Mind;
it('can be declared in different ways', function() {
List = db.define('List', {name: String});
Item = db.define('Item', {name: String});
Fear = db.define('Fear');
Mind = db.define('Mind');
// syntax 1 (old)
Item.belongsTo(List);
Object.keys((new Item).toObject()).should.include('listId');
(new Item).list.should.be.an.instanceOf(Function);
// syntax 2 (new)
Fear.belongsTo('mind');
Object.keys((new Fear).toObject()).should.include('mindId');
(new Fear).mind.should.be.an.instanceOf(Function);
// (new Fear).mind.build().should.be.an.instanceOf(Mind);
});
it('can be declared in short form');
});