From 2db43c58e5b0cb4cc3a70b9a13eacebb90139987 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 16 Jun 2014 01:17:37 -0700 Subject: [PATCH] Add support for hasOne --- lib/relation-definition.js | 139 +++++++++++++++++++++++++++++++++++++ lib/relations.js | 4 ++ test/relations.test.js | 39 ++++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 09849d12..ae030143 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -656,3 +656,142 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through}); }; + +/** + * HasOne + * @param modelFrom + * @param modelTo + * @param params + */ +RelationDefinition.hasOne = function (modelFrom, modelTo, params) { + params = params || {}; + if ('string' === typeof modelTo) { + params.as = modelTo; + if (params.model) { + modelTo = params.model; + } else { + var modelToName = modelTo.toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); + } + } + + var pk = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; + var relationName = params.as || i8n.camelize(modelTo.modelName, true); + + var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); + + var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ + name: relationName, + type: RelationTypes.hasOne, + modelFrom: modelFrom, + keyFrom: pk, + keyTo: fk, + modelTo: modelTo + }); + + modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); + + // Define a property for the scope so that we have 'this' for the scoped methods + Object.defineProperty(modelFrom.prototype, relationName, { + enumerable: true, + configurable: true, + get: function() { + var relation = new HasOne(relationDef, this); + var relationMethod = relation.related.bind(relation) + relationMethod.create = relation.create.bind(relation); + relationMethod.build = relation.build.bind(relation); + return relationMethod; + } + }); +}; + +HasOne.prototype.create = function(targetModelData, cb) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + + targetModelData = targetModelData || {}; + targetModelData[fk] = modelInstance[pk]; + modelTo.create(targetModelData, function(err, targetModel) { + if(!err) { + cb && cb(err, targetModel); + } else { + cb && cb(err); + } + }); +}; + +HasOne.prototype.build = function(targetModelData) { + var modelTo = this.definition.modelTo; + var pk = this.definition.keyFrom; + var fk = this.definition.keyTo; + targetModelData = targetModelData || {}; + targetModelData[fk] = this.modelInstance[pk]; + return new modelTo(targetModelData); +}; + +/** + * Define the method for the hasOne relation itself + * It will support one of the following styles: + * - order.customer(refresh, callback): Load the target model instance asynchronously + * - order.customer(customer): Synchronous setter of the target model instance + * - order.customer(): Synchronous getter of the target model instance + * + * @param refresh + * @param params + * @returns {*} + */ +HasOne.prototype.related = function (refresh, params) { + var modelTo = this.definition.modelTo; + var fk = this.definition.keyTo; + var pk = this.definition.keyFrom; + var modelInstance = this.modelInstance; + var relationName = this.definition.name; + + if (arguments.length === 1) { + params = refresh; + refresh = false; + } else if (arguments.length > 2) { + throw new Error('Method can\'t be called with more than two arguments'); + } + + var cachedValue; + if (!refresh && modelInstance.__cachedRelations + && (modelInstance.__cachedRelations[relationName] !== undefined)) { + cachedValue = modelInstance.__cachedRelations[relationName]; + } + if (params instanceof ModelBaseClass) { // acts as setter + params[fk] = modelInstance[pk]; + modelInstance.__cachedRelations[relationName] = params; + } else if (typeof params === 'function') { // acts as async getter + var cb = params; + if (cachedValue === undefined) { + var query = {where: {}}; + query.where[fk] = modelInstance[pk]; + modelTo.findOne(query, function (err, inst) { + if (err) { + return cb(err); + } + if (!inst) { + return cb(null, null); + } + // Check if the foreign key matches the primary key + if (inst[fk] === modelInstance[pk]) { + cb(null, inst); + } else { + cb(new Error('Permission denied')); + } + }); + return modelInstance[pk]; + } else { + cb(null, cachedValue); + return cachedValue; + } + } else if (params === undefined) { // acts as sync getter + return modelInstance[pk]; + } else { // setter + params[fk] = modelInstance[pk]; + delete modelInstance.__cachedRelations[relationName]; + } +}; diff --git a/lib/relations.js b/lib/relations.js index 8b740359..068bce7f 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -157,3 +157,7 @@ RelationMixin.belongsTo = function (modelTo, params) { RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) { RelationDefinition.hasAndBelongsToMany(this, modelTo, params); }; + +RelationMixin.hasOne = function hasMany(modelTo, params) { + RelationDefinition.hasOne(this, modelTo, params); +}; diff --git a/test/relations.test.js b/test/relations.test.js index 7bc3a1b3..d0cd0ea9 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -1,7 +1,7 @@ // This test written in mocha+should.js var should = require('./init.js'); -var db, Book, Chapter, Author, Reader; +var db, Book, Chapter, Author, Reader, Publisher; describe('relations', function () { before(function (done) { @@ -180,6 +180,43 @@ describe('relations', function () { }); + describe('hasOne', function () { + var Supplier, Account; + + before(function () { + db = getSchema(); + Supplier = db.define('Supplier', {name: String}); + Account = db.define('Account', {accountNo: String}); + }); + + it('can be declared using hasOne method', function () { + Supplier.hasOne(Account); + Object.keys((new Account()).toObject()).should.include('supplierId'); + (new Supplier()).account.should.be.an.instanceOf(Function); + + }); + + it('can be used to query data', function (done) { + // Supplier.hasOne(Account); + db.automigrate(function () { + Supplier.create({name: 'Supplier 1'}, function (e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account.create({accountNo: 'a01'}, function (err, account) { + supplier.account(function (e, act) { + should.not.exist(e); + should.exist(act); + act.should.be.an.instanceOf(Account); + supplier.account().should.equal(act.id); + done(); + }); + }); + }); + }); + }); + + }); + describe('hasAndBelongsToMany', function () { var Article, Tag, ArticleTag; it('can be declared', function (done) {