diff --git a/lib/dao.js b/lib/dao.js index 4ed71b3e..6595a576 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -341,31 +341,10 @@ DataAccessObject.findByIds = function(ids, cond, cb) { filter.where[pk] = { inq: ids }; mergeQuery(filter, cond || {}); this.find(filter, function(err, results) { - cb(err, err ? results : this.sortByIds(ids, results)); + cb(err, err ? results : utils.sortObjectsByIds(pk, ids, results)); }.bind(this)); }; -DataAccessObject.sortByIds = function(ids, results) { - var pk = this.dataSource.idName(this.modelName) || 'id'; - ids = ids.map(function(id) { - return (typeof id === 'object') ? id.toString() : id; - }); - - results.sort(function(x, y) { - var idA = (typeof x[pk] === 'object') ? x[pk].toString() : x[pk]; - var idB = (typeof y[pk] === 'object') ? y[pk].toString() : y[pk]; - var a = ids.indexOf(idA); - var b = ids.indexOf(idB); - if (a === -1 || b === -1) return 1; // last - if (a !== b) { - if (a > b) return 1; - if (a < b) return -1; - } - }); - - return results; -}; - function convertNullToNotFoundError(ctx, cb) { if (ctx.result !== null) return cb(); @@ -1175,6 +1154,10 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb // Convert the properties by type inst[key] = data[key]; typedData[key] = inst[key]; + if (typeof typedData[key] === 'object' + && typeof typedData[key].toObject === 'function') { + typedData[key] = typedData[key].toObject(); + } } inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), diff --git a/lib/datasource.js b/lib/datasource.js index b488ce72..9602ec4f 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -3,6 +3,7 @@ */ var ModelBuilder = require('./model-builder.js').ModelBuilder; var ModelDefinition = require('./model-definition.js'); +var RelationDefinition = require('./relation-definition.js'); var jutil = require('./jutil'); var utils = require('./utils'); var ModelBaseClass = require('./model.js'); @@ -364,7 +365,7 @@ function isModelClass(cls) { return cls.prototype instanceof ModelBaseClass; } -DataSource.relationTypes = ['belongsTo', 'hasMany', 'hasAndBelongsToMany', 'hasOne']; +DataSource.relationTypes = Object.keys(RelationDefinition.RelationTypes); function isModelDataSourceAttached(model) { return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource); diff --git a/lib/model-builder.js b/lib/model-builder.js index 73936be5..708d328b 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -501,9 +501,9 @@ ModelBuilder.prototype.defineValueType = function(type, aliases) { * * @param {String} model Name of model * @options {Object} properties JSON object specifying properties. Each property is a key whos value is - * either the [type](http://docs.strongloop.com/display/DOC/LDL+data+types) or `propertyName: {options}` + * either the [type](http://docs.strongloop.com/display/LB/LoopBack+types) or `propertyName: {options}` * where the options are described below. - * @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/DOC/LDL+data+types). + * @property {String} type Datatype of property: Must be an [LDL type](http://docs.strongloop.com/display/LB/LoopBack+types). * @property {Boolean} index True if the property is an index; false otherwise. */ ModelBuilder.prototype.extendModel = function (model, props) { diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 1171b998..e11b28bc 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -9,6 +9,7 @@ var mergeQuery = require('./scope.js').mergeQuery; var ModelBaseClass = require('./model.js'); var applyFilter = require('./connectors/memory').applyFilter; var ValidationError = require('./validations.js').ValidationError; +var debug = require('debug')('loopback:relations'); exports.Relation = Relation; exports.RelationDefinition = RelationDefinition; @@ -63,12 +64,15 @@ function extendScopeMethods(definition, scopeMethods, ext) { if (typeof ext === 'function') { customMethods = ext.call(definition, scopeMethods, relationClass); } else if (typeof ext === 'object') { - for (var key in ext) { - var relationMethod = ext[key]; - var method = scopeMethods[key] = function () { + function createFunc(definition, relationMethod) { + return function() { var relation = new relationClass(definition, this); return relationMethod.apply(relation, arguments); }; + }; + for (var key in ext) { + var relationMethod = ext[key]; + var method = scopeMethods[key] = createFunc(definition, relationMethod); if (relationMethod.shared) { sharedMethod(definition, key, method, relationMethod); } @@ -89,7 +93,6 @@ function RelationDefinition(definition) { } definition = definition || {}; this.name = definition.name; - this.accessor = definition.accessor || this.name; assert(this.name, 'Relation name is missing'); this.type = normalizeType(definition.type); assert(this.type, 'Invalid relation type: ' + definition.type); @@ -98,8 +101,8 @@ function RelationDefinition(definition) { this.keyFrom = definition.keyFrom; this.modelTo = definition.modelTo; this.keyTo = definition.keyTo; - this.discriminator = definition.discriminator; - if (!this.discriminator) { + this.polymorphic = definition.polymorphic; + if (typeof this.polymorphic !== 'object') { assert(this.modelTo, 'Target model is required'); } this.modelThrough = definition.modelThrough; @@ -128,6 +131,29 @@ RelationDefinition.prototype.toJSON = function () { return json; }; +/** + * Define a relation scope method + * @param {String} name of the method + * @param {Function} function to define + */ +RelationDefinition.prototype.defineMethod = function(name, fn) { + var relationClass = RelationClasses[this.type]; + var relationName = this.name; + var modelFrom = this.modelFrom; + var definition = this; + var scope = this.modelFrom.scopes[this.name]; + if (!scope) throw new Error('Unknown relation scope: ' + this.name); + var method = scope.defineMethod(name, function() { + var relation = new relationClass(definition, this); + return fn.apply(relation, arguments); + }); + if (fn.shared) { + sharedMethod(definition, name, method, fn); + modelFrom.prototype['__' + name + '__' + relationName] = method; + } + return method; +}; + /** * Apply the configured scope to the filter/query object. * @param {Object} modelInstance @@ -137,8 +163,13 @@ RelationDefinition.prototype.applyScope = function(modelInstance, filter) { filter = filter || {}; filter.where = filter.where || {}; if ((this.type !== 'belongsTo' || this.type === 'hasOne') - && typeof this.discriminator === 'string') { // polymorphic - filter.where[this.discriminator] = this.modelFrom.modelName; + && typeof this.polymorphic === 'object') { // polymorphic + var discriminator = this.polymorphic.discriminator; + if (this.polymorphic.invert) { + filter.where[discriminator] = this.modelTo.modelName; + } else { + filter.where[discriminator] = this.modelFrom.modelName; + } } if (typeof this.scope === 'function') { var scope = this.scope.call(this, modelInstance, filter); @@ -155,21 +186,30 @@ RelationDefinition.prototype.applyScope = function(modelInstance, filter) { * @param {Object} modelInstance * @param {Object} target */ -RelationDefinition.prototype.applyProperties = function(modelInstance, target) { +RelationDefinition.prototype.applyProperties = function(modelInstance, obj) { + var source = modelInstance, target = obj; + if (this.options.invertProperties) { + source = obj, target = modelInstance; + } if (typeof this.properties === 'function') { - var data = this.properties.call(this, modelInstance); + var data = this.properties.call(this, source); for(var k in data) { target[k] = data[k]; } } else if (typeof this.properties === 'object') { for(var k in this.properties) { var key = this.properties[k]; - target[key] = modelInstance[k]; + target[key] = source[k]; } } if ((this.type !== 'belongsTo' || this.type === 'hasOne') - && typeof this.discriminator === 'string') { // polymorphic - target[this.discriminator] = this.modelFrom.modelName; + && typeof this.polymorphic === 'object') { // polymorphic + var discriminator = this.polymorphic.discriminator; + if (this.polymorphic.invert) { + target[discriminator] = this.modelTo.modelName; + } else { + target[discriminator] = this.modelFrom.modelName; + } } }; @@ -201,6 +241,15 @@ Relation.prototype.getCache = function () { return this.modelInstance.__cachedRelations[this.definition.name]; }; +/** + * Fetch the related model(s) - this is a helper method to unify access. + * @param (Boolean|Object} condOrRefresh refresh or conditions object + * @param {Function} cb callback + */ +Relation.prototype.fetch = function(condOrRefresh, cb) { + this.modelInstance[this.definition.name].apply(this.modelInstance, arguments); +}; + /** * HasMany subclass * @param {RelationDefinition|Object} definition @@ -398,6 +447,21 @@ function lookupModel(models, modelName) { } } +function lookupModelTo(modelFrom, modelTo, params, singularize) { + if ('string' === typeof modelTo) { + params.as = params.as || modelTo; + modelTo = params.model || modelTo; + if (typeof modelTo === 'string') { + var modelToName = (singularize ? i8n.singularize(modelTo) : modelTo).toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo; + } + if (typeof modelTo !== 'function') { + throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); + } + } + return modelTo; +} + /*! * Normalize polymorphic parameters * @param {Object|String} params Name of the polymorphic relation or params @@ -436,24 +500,17 @@ function polymorphicParams(params) { RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { var thisClassName = modelFrom.modelName; params = params || {}; - if (typeof modelTo === 'string') { - params.as = modelTo; - if (params.model) { - modelTo = params.model; - } else { - var modelToName = i8n.singularize(modelTo).toLowerCase(); - modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); - } - } + modelTo = lookupModelTo(modelFrom, modelTo, params, true); var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true); var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; - var discriminator; + var discriminator, polymorphic; if (params.polymorphic) { - var polymorphic = polymorphicParams(params.polymorphic); + polymorphic = polymorphicParams(params.polymorphic); + polymorphic.invert = !!params.invert; discriminator = polymorphic.discriminator; if (!params.invert) { fk = polymorphic.foreignKey; @@ -469,12 +526,12 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: idName, keyTo: fk, - discriminator: discriminator, modelTo: modelTo, multiple: true, properties: params.properties, scope: params.scope, - options: params.options + options: params.options, + polymorphic: polymorphic }); definition.modelThrough = params.through; @@ -554,7 +611,8 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { return filter; }, scopeMethods, definition.options); - + + return definition; }; function scopeMethod(definition, methodName) { @@ -689,9 +747,13 @@ var throughKeys = function(definition) { var modelThrough = definition.modelThrough; var pk2 = definition.modelTo.definition.idName(); - if (definition.discriminator) { // polymorphic + if (typeof definition.polymorphic === 'object') { // polymorphic var fk1 = definition.keyTo; - var fk2 = definition.keyThrough; + if (definition.polymorphic.invert) { + var fk2 = definition.polymorphic.foreignKey; + } else { + var fk2 = definition.keyThrough; + } } else { var fk1 = findBelongsTo(modelThrough, definition.modelFrom, definition.keyFrom); @@ -953,13 +1015,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { var discriminator, polymorphic; params = params || {}; if ('string' === typeof modelTo && !params.polymorphic) { - params.as = modelTo; - if (params.model) { - modelTo = params.model; - } else { - var modelToName = modelTo.toLowerCase(); - modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); - } + modelTo = lookupModelTo(modelFrom, modelTo, params); } var idName, relationName, fk; @@ -993,17 +1049,17 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName); } - var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ + var definition = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, type: RelationTypes.belongsTo, modelFrom: modelFrom, keyFrom: fk, keyTo: idName, - discriminator: discriminator, modelTo: modelTo, properties: params.properties, scope: params.scope, - options: params.options + options: params.options, + polymorphic: polymorphic }); // Define a property for the scope so that we have 'this' for the scoped methods @@ -1011,12 +1067,12 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { enumerable: true, configurable: true, get: function() { - var relation = new BelongsTo(relationDef, this); + var relation = new BelongsTo(definition, this); var relationMethod = relation.related.bind(relation); relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); - if (relationDef.modelTo) { - relationMethod._targetClass = relationDef.modelTo.modelName; + if (definition.modelTo) { + relationMethod._targetClass = definition.modelTo.modelName; } return relationMethod; } @@ -1030,6 +1086,8 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { f.apply(this, arguments); }; modelFrom.prototype['__get__' + relationName] = fn; + + return definition; }; BelongsTo.prototype.create = function(targetModelData, cb) { @@ -1043,14 +1101,17 @@ BelongsTo.prototype.create = function(targetModelData, cb) { cb = targetModelData; targetModelData = {}; } - + this.definition.applyProperties(modelInstance, targetModelData || {}); modelTo.create(targetModelData, function(err, targetModel) { if(!err) { modelInstance[fk] = targetModel[pk]; - self.resetCache(targetModel); - cb && cb(err, targetModel); + modelInstance.save(function(err, inst) { + if (cb && err) return cb && cb(err); + self.resetCache(targetModel); + cb && cb(err, targetModel); + }); } else { cb && cb(err); } @@ -1078,10 +1139,10 @@ BelongsTo.prototype.related = function (refresh, params) { var self = this; var modelFrom = this.definition.modelFrom; var modelTo = this.definition.modelTo; - var discriminator = this.definition.discriminator; var pk = this.definition.keyTo; var fk = this.definition.keyFrom; var modelInstance = this.modelInstance; + var discriminator; if (arguments.length === 1) { params = refresh; @@ -1090,6 +1151,10 @@ BelongsTo.prototype.related = function (refresh, params) { throw new Error('Method can\'t be called with more than two arguments'); } + if (typeof this.definition.polymorphic === 'object') { + discriminator = this.definition.polymorphic.discriminator; + } + var cachedValue; if (!refresh) { cachedValue = self.getCache(); @@ -1097,11 +1162,12 @@ BelongsTo.prototype.related = function (refresh, params) { if (params instanceof ModelBaseClass) { // acts as setter modelTo = params.constructor; modelInstance[fk] = params[pk]; - if (discriminator) modelInstance[discriminator] = params.constructor.modelName; - var data = {}; - this.definition.applyProperties(params, data); - modelInstance.setAttributes(data); + if (discriminator) { + modelInstance[discriminator] = params.constructor.modelName; + } + + this.definition.applyProperties(modelInstance, params); self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter @@ -1182,19 +1248,9 @@ BelongsTo.prototype.related = function (refresh, params) { */ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) { params = params || {}; + modelTo = lookupModelTo(modelFrom, modelTo, params, true); + var models = modelFrom.dataSource.modelBuilder.models; - - if ('string' === typeof modelTo) { - params.as = modelTo; - if (params.model) { - modelTo = params.model; - } else { - modelTo = lookupModel(models, i8n.singularize(modelTo)) || modelTo; - } - if (typeof modelTo === 'string') { - throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); - } - } if (!params.through) { if (params.polymorphic) throw new Error('Polymorphic relations need a through model'); @@ -1222,8 +1278,7 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, params.through.belongsTo(modelTo); - this.hasMany(modelFrom, modelTo, options); - + return this.hasMany(modelFrom, modelTo, options); }; /** @@ -1243,24 +1298,16 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, */ 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); - } - } + modelTo = lookupModelTo(modelFrom, modelTo, params); 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 discriminator; + var discriminator, polymorphic; if (params.polymorphic) { - var polymorphic = polymorphicParams(params.polymorphic); + polymorphic = polymorphicParams(params.polymorphic); fk = polymorphic.foreignKey; discriminator = polymorphic.discriminator; if (!params.through) { @@ -1268,16 +1315,16 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { } } - var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ + var definition = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, type: RelationTypes.hasOne, modelFrom: modelFrom, keyFrom: pk, keyTo: fk, - discriminator: discriminator, modelTo: modelTo, properties: params.properties, - options: params.options + options: params.options, + polymorphic: polymorphic }); modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); @@ -1287,11 +1334,11 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { enumerable: true, configurable: true, get: function() { - var relation = new HasOne(relationDef, this); + var relation = new HasOne(definition, this); var relationMethod = relation.related.bind(relation) relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); - relationMethod._targetClass = relationDef.modelTo.modelName; + relationMethod._targetClass = definition.modelTo.modelName; return relationMethod; } }); @@ -1304,6 +1351,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { f.apply(this, arguments); }; modelFrom.prototype['__get__' + relationName] = fn; + + return definition; }; /** @@ -1476,30 +1525,26 @@ HasOne.prototype.related = function (refresh, params) { }; RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) { - var thisClassName = modelFrom.modelName; params = params || {}; - if (typeof modelTo === 'string') { - params.as = modelTo; - if (params.model) { - modelTo = params.model; - } else { - var modelToName = i8n.singularize(modelTo).toLowerCase(); - modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); - } + modelTo = lookupModelTo(modelFrom, modelTo, params, true); + + var thisClassName = modelFrom.modelName; + var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List'); + var propertyName = params.property || i8n.camelize(modelTo.pluralModelName, true); + var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + + if (relationName === propertyName) { + propertyName = '_' + propertyName; + debug('EmbedsMany property cannot be equal to relation name: ' + + 'forcing property %s for relation %s', propertyName, relationName); } - var accessorName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List'); - var relationName = params.property || i8n.camelize(modelTo.pluralModelName, true); - var fk = modelTo.dataSource.idName(modelTo.modelName) || 'id'; - var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; - - var definition = modelFrom.relations[accessorName] = new RelationDefinition({ - accessor: accessorName, + var definition = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, type: RelationTypes.embedsMany, modelFrom: modelFrom, - keyFrom: idName, - keyTo: fk, + keyFrom: propertyName, + keyTo: idName, modelTo: modelTo, multiple: true, properties: params.properties, @@ -1508,7 +1553,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) embed: true }); - modelFrom.dataSource.defineProperty(modelFrom.modelName, relationName, { + modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, { type: [modelTo], default: function() { return []; } }); @@ -1516,14 +1561,14 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) modelTo.validatesPresenceOf(idName); if (!params.polymorphic) { - modelFrom.validate(relationName, function(err) { - var embeddedList = this[relationName] || []; + modelFrom.validate(propertyName, function(err) { + var embeddedList = this[propertyName] || []; var ids = embeddedList.map(function(m) { return m[idName]; }); var uniqueIds = ids.filter(function(id, pos) { return ids.indexOf(id) === pos; }); if (ids.length !== uniqueIds.length) { - this.errors.add(relationName, 'Contains duplicate `' + idName + '`', 'uniqueness'); + this.errors.add(propertyName, 'Contains duplicate `' + idName + '`', 'uniqueness'); err(false); } }, { code: 'uniqueness' }) @@ -1531,9 +1576,9 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) // validate all embedded items if (definition.options.validate) { - modelFrom.validate(relationName, function(err) { + modelFrom.validate(propertyName, function(err) { var self = this; - var embeddedList = this[relationName] || []; + var embeddedList = this[propertyName] || []; var hasErrors = false; embeddedList.forEach(function(item) { if (item instanceof modelTo) { @@ -1543,11 +1588,11 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) var first = Object.keys(item.errors)[0]; var msg = 'contains invalid item: `' + id + '`'; msg += ' (' + first + ' ' + item.errors[first] + ')'; - self.errors.add(relationName, msg, 'invalid'); + self.errors.add(propertyName, msg, 'invalid'); } } else { hasErrors = true; - self.errors.add(relationName, 'Contains invalid item', 'invalid'); + self.errors.add(propertyName, 'Contains invalid item', 'invalid'); } }); if (hasErrors) err(false); @@ -1568,19 +1613,19 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) }; var findByIdFunc = scopeMethods.findById; - modelFrom.prototype['__findById__' + accessorName] = findByIdFunc; + modelFrom.prototype['__findById__' + relationName] = findByIdFunc; var destroyByIdFunc = scopeMethods.destroy; - modelFrom.prototype['__destroyById__' + accessorName] = destroyByIdFunc; + modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc; var updateByIdFunc = scopeMethods.updateById; - modelFrom.prototype['__updateById__' + accessorName] = updateByIdFunc; + modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc; var addFunc = scopeMethods.add; - modelFrom.prototype['__link__' + accessorName] = addFunc; + modelFrom.prototype['__link__' + relationName] = addFunc; var removeFunc = scopeMethods.remove; - modelFrom.prototype['__unlink__' + accessorName] = removeFunc; + modelFrom.prototype['__unlink__' + relationName] = removeFunc; scopeMethods.create = scopeMethod(definition, 'create'); scopeMethods.build = scopeMethod(definition, 'build'); @@ -1593,23 +1638,67 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) var methodName = customMethods[i]; var method = scopeMethods[methodName]; if (typeof method === 'function' && method.shared === true) { - modelFrom.prototype['__' + methodName + '__' + accessorName] = method; + modelFrom.prototype['__' + methodName + '__' + relationName] = method; } }; // Mix the property and scoped methods into the prototype class - var scopeDefinition = defineScope(modelFrom.prototype, modelTo, accessorName, function () { + var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () { return {}; }, scopeMethods, definition.options); scopeDefinition.related = scopeMethods.related; + + return definition; +}; + +EmbedsMany.prototype.prepareEmbeddedInstance = function(inst) { + if (inst && inst.triggerParent !== 'function') { + var self = this; + var propertyName = this.definition.keyFrom; + var modelInstance = this.modelInstance; + inst.triggerParent = function(actionName, callback) { + if (actionName === 'save' || actionName === 'destroy') { + var embeddedList = self.embeddedList(); + if (actionName === 'destroy') { + var index = embeddedList.indexOf(inst); + if (index > -1) embeddedList.splice(index, 1); + } + modelInstance.updateAttribute(propertyName, + embeddedList, function(err, modelInst) { + callback(err, err ? null : modelInst); + }); + } else { + process.nextTick(callback); + } + }; + var originalTrigger = inst.trigger; + inst.trigger = function(actionName, work, data, callback) { + if (typeof work === 'function') { + var originalWork = work; + work = function(next) { + originalWork.call(this, function(done) { + inst.triggerParent(actionName, function(err, inst) { + next(done); // TODO [fabien] - error handling? + }); + }); + }; + } + originalTrigger.call(this, actionName, work, data, callback); + }; + } +}; + +EmbedsMany.prototype.embeddedList = function(modelInstance) { + modelInstance = modelInstance || this.modelInstance; + var embeddedList = modelInstance[this.definition.keyFrom] || []; + embeddedList.forEach(this.prepareEmbeddedInstance.bind(this)); + return embeddedList; }; EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { var modelTo = this.definition.modelTo; - var relationName = this.definition.name; var modelInstance = this.modelInstance; - var self = receiver; var actualCond = {}; var actualRefresh = false; @@ -1626,13 +1715,13 @@ EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb throw new Error('Method can be only called with one or two arguments'); } - var embeddedList = self[relationName] || []; + var embeddedList = this.embeddedList(receiver); - this.definition.applyScope(modelInstance, actualCond); + this.definition.applyScope(receiver, actualCond); var params = mergeQuery(actualCond, scopeParams); - if (params.where) { + if (params.where) { // TODO [fabien] Support order/sorting embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList; } @@ -1648,12 +1737,11 @@ EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb }; EmbedsMany.prototype.findById = function (fkId, cb) { - var pk = this.definition.keyFrom; + var pk = this.definition.keyTo; var modelTo = this.definition.modelTo; - var relationName = this.definition.name; var modelInstance = this.modelInstance; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); var find = function(id) { for (var i = 0; i < embeddedList.length; i++) { @@ -1690,18 +1778,16 @@ EmbedsMany.prototype.updateById = function (fkId, data, cb) { } var modelTo = this.definition.modelTo; - var relationName = this.definition.name; + var propertyName = this.definition.keyFrom; var modelInstance = this.modelInstance; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); var inst = this.findById(fkId); if (inst instanceof modelTo) { if (typeof data === 'object') { - for (var key in data) { - inst[key] = data[key]; - } + inst.setAttributes(data); } var err = inst.isValid() ? null : new ValidationError(inst); if (err && typeof cb === 'function') { @@ -1711,7 +1797,7 @@ EmbedsMany.prototype.updateById = function (fkId, data, cb) { } if (typeof cb === 'function') { - modelInstance.updateAttribute(relationName, + modelInstance.updateAttribute(propertyName, embeddedList, function(err) { cb(err, inst); }); @@ -1726,10 +1812,10 @@ EmbedsMany.prototype.updateById = function (fkId, data, cb) { EmbedsMany.prototype.destroyById = function (fkId, cb) { var modelTo = this.definition.modelTo; - var relationName = this.definition.name; + var propertyName = this.definition.keyFrom; var modelInstance = this.modelInstance; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId); @@ -1737,7 +1823,7 @@ EmbedsMany.prototype.destroyById = function (fkId, cb) { var index = embeddedList.indexOf(inst); if (index > -1) embeddedList.splice(index, 1); if (typeof cb === 'function') { - modelInstance.updateAttribute(relationName, + modelInstance.updateAttribute(propertyName, embeddedList, function(err) { cb(err); }); @@ -1754,10 +1840,9 @@ EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById; EmbedsMany.prototype.at = function (index, cb) { var modelTo = this.definition.modelTo; - var relationName = this.definition.name; var modelInstance = this.modelInstance; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); var item = embeddedList[parseInt(index)]; item = (item instanceof modelTo) ? item : null; @@ -1772,9 +1857,9 @@ EmbedsMany.prototype.at = function (index, cb) { }; EmbedsMany.prototype.create = function (targetModelData, cb) { - var pk = this.definition.keyFrom; + var pk = this.definition.keyTo; var modelTo = this.definition.modelTo; - var relationName = this.definition.name; + var propertyName = this.definition.keyFrom; var modelInstance = this.modelInstance; var autoId = this.definition.options.autoId !== false; @@ -1784,7 +1869,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { } targetModelData = targetModelData || {}; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); var inst = this.build(targetModelData); @@ -1796,20 +1881,19 @@ EmbedsMany.prototype.create = function (targetModelData, cb) { }); } - modelInstance.updateAttribute(relationName, + modelInstance.updateAttribute(propertyName, embeddedList, function(err, modelInst) { cb(err, err ? null : inst); }); }; EmbedsMany.prototype.build = function(targetModelData) { - var pk = this.definition.keyFrom; + var pk = this.definition.keyTo; var modelTo = this.definition.modelTo; - var relationName = this.definition.name; var modelInstance = this.modelInstance; var autoId = this.definition.options.autoId !== false; - var embeddedList = modelInstance[relationName] || []; + var embeddedList = this.embeddedList(); targetModelData = targetModelData || {}; @@ -1834,6 +1918,8 @@ EmbedsMany.prototype.build = function(targetModelData) { embeddedList.push(inst); } + this.prepareEmbeddedInstance(inst); + return inst; }; @@ -1911,7 +1997,7 @@ EmbedsMany.prototype.remove = function (acInst, cb) { belongsTo.applyScope(modelInstance, filter); - modelInstance[definition.accessor](filter, function(err, items) { + modelInstance[definition.name](filter, function(err, items) { if (err) return cb(err); items.forEach(function(item) { @@ -1925,18 +2011,10 @@ EmbedsMany.prototype.remove = function (acInst, cb) { }; RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) { - var thisClassName = modelFrom.modelName; params = params || {}; - if (typeof modelTo === 'string') { - params.as = modelTo; - if (params.model) { - modelTo = params.model; - } else { - var modelToName = i8n.singularize(modelTo).toLowerCase(); - modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); - } - } + modelTo = lookupModelTo(modelFrom, modelTo, params, true); + var thisClassName = modelFrom.modelName; var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true); var fk = params.foreignKey || i8n.camelize(modelTo.modelName + '_ids', true); var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id'; @@ -2017,6 +2095,8 @@ RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, }, scopeMethods, definition.options); scopeDefinition.related = scopeMethods.related; // bound to definition + + return definition; }; ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { diff --git a/lib/relations.js b/lib/relations.js index d3e735ff..ba9a36d7 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -65,7 +65,7 @@ function RelationMixin() { * @property {Object} model Model object */ RelationMixin.hasMany = function hasMany(modelTo, params) { - RelationDefinition.hasMany(this, modelTo, params); + return RelationDefinition.hasMany(this, modelTo, params); }; /** @@ -120,7 +120,7 @@ RelationMixin.hasMany = function hasMany(modelTo, params) { * */ RelationMixin.belongsTo = function (modelTo, params) { - RelationDefinition.belongsTo(this, modelTo, params); + return RelationDefinition.belongsTo(this, modelTo, params); }; /** @@ -155,17 +155,17 @@ RelationMixin.belongsTo = function (modelTo, params) { * @property {Object} model Model object */ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) { - RelationDefinition.hasAndBelongsToMany(this, modelTo, params); + return RelationDefinition.hasAndBelongsToMany(this, modelTo, params); }; RelationMixin.hasOne = function hasMany(modelTo, params) { - RelationDefinition.hasOne(this, modelTo, params); + return RelationDefinition.hasOne(this, modelTo, params); }; RelationMixin.referencesMany = function hasMany(modelTo, params) { - RelationDefinition.referencesMany(this, modelTo, params); + return RelationDefinition.referencesMany(this, modelTo, params); }; RelationMixin.embedsMany = function hasMany(modelTo, params) { - RelationDefinition.embedsMany(this, modelTo, params); + return RelationDefinition.embedsMany(this, modelTo, params); }; diff --git a/lib/scope.js b/lib/scope.js index 0c5ea86c..d5f6b38e 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -55,6 +55,15 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres } } +/** + * Define a scope method + * @param {String} name of the method + * @param {Function} function to define + */ +ScopeDefinition.prototype.defineMethod = function(name, fn) { + return this.methods[name] = fn; +} + /** * Define a scope to the class * @param {Model} cls The class where the scope method is added @@ -138,6 +147,7 @@ function defineScope(cls, targetClass, name, params, methods, options) { f.build = build; f.create = create; f.destroyAll = destroyAll; + f.count = count; for (var i in definition.methods) { f[i] = definition.methods[i].bind(self); } @@ -183,6 +193,13 @@ function defineScope(cls, targetClass, name, params, methods, options) { cls['__delete__' + name] = fn_delete; + var fn_count = function (cb) { + var f = this[name].count; + f.apply(this[name], arguments); + }; + + cls['__count__' + name] = fn_count; + /* * Extracting fixed property values for the scope from the where clause into * the data object @@ -239,6 +256,11 @@ function defineScope(cls, targetClass, name, params, methods, options) { var where = (this._scope && this._scope.where) || {}; targetClass.destroyAll(where, cb); } + + function count(cb) { + var where = (this._scope && this._scope.where) || {}; + targetClass.count(where, cb); + } return definition; } @@ -277,11 +299,11 @@ function mergeQuery(base, update) { base.collect = update.collect; } - // overwrite order - if (update.order) { + // set order + if (!base.order && update.order) { base.order = update.order; } - + // overwrite pagination if (update.limit !== undefined) { base.limit = update.limit; diff --git a/lib/utils.js b/lib/utils.js index 520cf462..6afb9129 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -6,6 +6,7 @@ exports.parseSettings = parseSettings; exports.mergeSettings = mergeSettings; exports.isPlainObject = isPlainObject; exports.defineCachedRelations = defineCachedRelations; +exports.sortObjectsByIds = sortObjectsByIds; var traverse = require('traverse'); @@ -203,4 +204,40 @@ function defineCachedRelations(obj) { function isPlainObject(obj) { return (typeof obj === 'object') && (obj !== null) && (obj.constructor === Object); -} \ No newline at end of file +} + + + +function sortObjectsByIds(idName, ids, objects, strict) { + ids = ids.map(function(id) { + return (typeof id === 'object') ? id.toString() : id; + }); + + var indexOf = function(x) { + var isObj = (typeof x[idName] === 'object'); // ObjectID + var id = isObj ? x[idName].toString() : x[idName]; + return ids.indexOf(id); + }; + + var heading = []; + var tailing = []; + + objects.forEach(function(x) { + if (typeof x === 'object') { + var idx = indexOf(x); + if (strict && idx === -1) return; + idx === -1 ? tailing.push(x) : heading.push(x); + } + }); + + heading.sort(function(x, y) { + var a = indexOf(x); + var b = indexOf(y); + if (a === -1 || b === -1) return 1; // last + if (a === b) return 0; + if (a > b) return 1; + if (a < b) return -1; + }); + + return heading.concat(tailing); +}; diff --git a/package.json b/package.json index a4e4fc46..67e5064f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "2.3.1", + "version": "2.4.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 18f0e0ad..458f91db 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -903,6 +903,26 @@ describe('Load models with relations', function () { assert(Post.relations['user']); done(); }); + + it('should set up referencesMany relations', function (done) { + var ds = new DataSource('memory'); + + var Post = ds.define('Post', {userId: Number, content: String}); + var User = ds.define('User', {name: String}, {relations: {posts: {type: 'referencesMany', model: 'Post'}}}); + + assert(User.relations['posts']); + done(); + }); + + it('should set up embedsMany relations', function (done) { + var ds = new DataSource('memory'); + + var Post = ds.define('Post', {userId: Number, content: String}); + var User = ds.define('User', {name: String}, {relations: {posts: {type: 'embedsMany', model: 'Post' }}}); + + assert(User.relations['posts']); + done(); + }); it('should set up foreign key with the correct type', function (done) { var ds = new DataSource('memory'); diff --git a/test/mixins.test.js b/test/mixins.test.js index f1dacf9d..e60f7e02 100644 --- a/test/mixins.test.js +++ b/test/mixins.test.js @@ -42,12 +42,15 @@ mixins.define('TimeStamp', timestamps); describe('Model class', function () { - it('should define a mixin', function() { + it('should define mixins', function() { mixins.define('Example', function(Model, options) { Model.prototype.example = function() { return options; }; }); + mixins.define('Demo', function(Model, options) { + Model.demoMixin = options.ok; + }); }); it('should apply a mixin class', function() { @@ -58,7 +61,7 @@ describe('Model class', function () { var memory = new DataSource('mem', {connector: Memory}, modelBuilder); var Item = memory.createModel('Item', { name: 'string' }, { - mixins: { TimeStamp: true, demo: true, Address: true } + mixins: { Address: true } }); var properties = Item.definition.properties; @@ -70,11 +73,12 @@ describe('Model class', function () { it('should apply mixins', function(done) { var memory = new DataSource('mem', {connector: Memory}, modelBuilder); var Item = memory.createModel('Item', { name: 'string' }, { - mixins: { TimeStamp: true, demo: { ok: true } } + mixins: { TimeStamp: true, Demo: { ok: true } } }); Item.mixin('Example', { foo: 'bar' }); - Item.mixin('other'); + + Item.demoMixin.should.be.true; var properties = Item.definition.properties; properties.createdAt.should.eql({ type: Date }); diff --git a/test/relations.test.js b/test/relations.test.js index d1a2003a..87610f84 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -537,6 +537,16 @@ describe('relations', function () { }); }); }); + + it('should find count of records on scope - scoped', function (done) { + Category.findOne(function (err, c) { + c.productType = 'tool'; // temporary, for scoping + c.products.count(function(err, count) { + count.should.equal(1); + done(); + }); + }); + }); it('should delete records on scope - scoped', function (done) { Category.findOne(function (err, c) { @@ -1030,6 +1040,40 @@ describe('relations', function () { }); }); }); + + it('should create polymorphic item through relation scope', function (done) { + Picture.findById(anotherPicture.id, function(err, p) { + p.authors.create({ name: 'Author 3' }, function(err, a) { + should.not.exist(err); + author = a; + author.name.should.equal('Author 3'); + done(); + }); + }); + }); + + it('should create polymorphic through model - new author', function (done) { + PictureLink.findOne({ where: { + pictureId: anotherPicture.id, imageableId: author.id, imageableType: 'Author' + } }, function(err, link) { + should.not.exist(err); + link.pictureId.should.eql(anotherPicture.id); + link.imageableId.should.eql(author.id); + link.imageableType.should.equal('Author'); + done(); + }); + }); + + it('should find polymorphic items - new author', function (done) { + Author.findById(author.id, function(err, author) { + author.pictures(function(err, pics) { + pics.should.have.length(1); + pics[0].id.should.eql(anotherPicture.id); + pics[0].name.should.equal('Example'); + done(); + }); + }); + }); }); @@ -1109,11 +1153,9 @@ describe('relations', function () { p.person.create({name: 'Fred', age: 36 }, function(err, person) { personCreated = person; p.personId.should.equal(person.id); - p.save(function (err, p) { - person.name.should.equal('Fred'); - person.passportNotes.should.equal('Some notes...'); - done(); - }); + person.name.should.equal('Fred'); + person.passportNotes.should.equal('Some notes...'); + done(); }); }); @@ -1191,7 +1233,6 @@ describe('relations', function () { Article.create(function (e, article) { article.tags.create({name: 'popular'}, function (e, t) { t.should.be.an.instanceOf(Tag); - // console.log(t); ArticleTag.findOne(function (e, at) { should.exist(at); at.tagId.toString().should.equal(t.id.toString()); @@ -1566,19 +1607,15 @@ describe('relations', function () { describe('embedsMany - relations, scope and properties', function () { - var product1, product2, product3; + var category, product1, product2, product3; - before(function (done) { + before(function () { db = getSchema(); Category = db.define('Category', {name: String}); Product = db.define('Product', {name: String}); Link = db.define('Link', {name: String}); - - db.automigrate(function () { - Person.destroyAll(done); - }); }); - + it('can be declared', function (done) { Category.embedsMany(Link, { as: 'items', // rename @@ -1588,6 +1625,7 @@ describe('relations', function () { Link.belongsTo(Product, { foreignKey: 'id', // re-use the actual product id properties: { id: 'id', name: 'name' }, // denormalize, transfer id + options: { invertProperties: true } }); db.automigrate(function() { Product.create({ name: 'Product 0' }, done); // offset ids for tests @@ -1607,7 +1645,7 @@ describe('relations', function () { }); }); - it('should create items on scope', function(done) { + it('should associate items on scope', function(done) { Category.create({ name: 'Category A' }, function(err, cat) { var link = cat.items.build(); link.product(product1); @@ -1718,12 +1756,84 @@ describe('relations', function () { }); }); - it('should remove embedded items by reference id', function(done) { + it('should have removed embedded items by reference id', function(done) { Category.findOne(function(err, cat) { cat.links.should.have.length(1); done(); }); }); + + var productId; + + it('should create items on scope', function(done) { + Category.create({ name: 'Category B' }, function(err, cat) { + category = cat; + var link = cat.items.build({ notes: 'Some notes...' }); + link.product.create({ name: 'Product 1' }, function(err, p) { + productId = p.id; + cat.links[0].id.should.eql(p.id); + cat.links[0].name.should.equal('Product 1'); // denormalized + cat.links[0].notes.should.equal('Some notes...'); + cat.items.at(0).should.equal(cat.links[0]); + done(); + }); + }); + }); + + it('should find items on scope', function(done) { + Category.findById(category.id, function(err, cat) { + cat.name.should.equal('Category B'); + cat.links.toObject().should.eql([ + {id: productId, name: 'Product 1', notes: 'Some notes...'} + ]); + cat.items.at(0).should.equal(cat.links[0]); + cat.items(function(err, items) { // alternative access + items.should.be.an.array; + items.should.have.length(1); + items[0].product(function(err, p) { + p.name.should.equal('Product 1'); // actual value + done(); + }); + }); + }); + }); + + it('should update items on scope - and save parent', function(done) { + Category.findById(category.id, function(err, cat) { + var link = cat.items.at(0); + link.updateAttributes({notes: 'Updated notes...'}, function(err, link) { + link.notes.should.equal('Updated notes...'); + done(); + }); + }); + }); + + it('should find items on scope - verify update', function(done) { + Category.findById(category.id, function(err, cat) { + cat.name.should.equal('Category B'); + cat.links.toObject().should.eql([ + {id: productId, name: 'Product 1', notes: 'Updated notes...'} + ]); + done(); + }); + }); + + it('should remove items from scope - and save parent', function(done) { + Category.findById(category.id, function(err, cat) { + cat.items.at(0).destroy(function(err, link) { + cat.links.should.eql([]); + done(); + }); + }); + }); + + it('should find items on scope - verify destroy', function(done) { + Category.findById(category.id, function(err, cat) { + cat.name.should.equal('Category B'); + cat.links.should.eql([]); + done(); + }); + }); }); @@ -1757,7 +1867,8 @@ describe('relations', function () { }); Link.belongsTo('linked', { polymorphic: true, // needs unique auto-id - properties: { name: 'name' } // denormalized + properties: { name: 'name' }, // denormalized + options: { invertProperties: true } }); db.automigrate(done); }); @@ -2129,5 +2240,80 @@ describe('relations', function () { }); }); + + describe('custom relation/scope methods', function () { + var categoryId; + + before(function (done) { + db = getSchema(); + Category = db.define('Category', {name: String}); + Product = db.define('Product', {name: String}); + + db.automigrate(function () { + Category.destroyAll(function() { + Product.destroyAll(done); + }); + }); + }); + + it('can be declared', function (done) { + var relation = Category.hasMany(Product); + + var summarize = function(cb) { + var modelInstance = this.modelInstance; + this.fetch(function(err, items) { + if (err) return cb(err, []); + var summary = items.map(function(item) { + var obj = item.toObject(); + obj.categoryName = modelInstance.name; + return obj; + }); + cb(null, summary); + }); + }; + + summarize.shared = true; // remoting + summarize.http = { verb: 'get', path: '/products/summary' }; + + relation.defineMethod('summarize', summarize); + + Category.prototype['__summarize__products'].should.be.a.function; + should.exist(Category.prototype['__summarize__products'].shared); + Category.prototype['__summarize__products'].http.should.eql(summarize.http); + + db.automigrate(done); + }); + + it('should setup test records', function (done) { + Category.create({ name: 'Category A' }, function(err, cat) { + categoryId = cat.id; + cat.products.create({ name: 'Product 1' }, function(err, p) { + cat.products.create({ name: 'Product 2' }, function(err, p) { + done(); + }); + }) + }); + }); + + it('should allow custom scope methods - summarize', function(done) { + var expected = [ + { name: 'Product 1', categoryId: categoryId, categoryName: 'Category A' }, + { name: 'Product 2', categoryId: categoryId, categoryName: 'Category A' } + ]; + + Category.findOne(function(err, cat) { + cat.products.summarize(function(err, summary) { + should.not.exist(err); + var result = summary.map(function(item) { + delete item.id; + return item; + }); + result.should.eql(expected); + done(); + }); + }) + }); + + }); }); diff --git a/test/scope.test.js b/test/scope.test.js index 2060c80e..c8033c82 100644 --- a/test/scope.test.js +++ b/test/scope.test.js @@ -76,3 +76,57 @@ describe('scope', function () { }); }); }); + +describe('scope - order', function () { + + before(function () { + db = getSchema(); + Station = db.define('Station', { + name: {type: String, index: true}, + order: {type: Number, index: true} + }); + Station.scope('reverse', {order: 'order DESC'}); + }); + + beforeEach(function (done) { + Station.destroyAll(done); + }); + + beforeEach(function (done) { + Station.create({ name: 'a', order: 1 }, done); + }); + + beforeEach(function (done) { + Station.create({ name: 'b', order: 2 }, done); + }); + + beforeEach(function (done) { + Station.create({ name: 'c', order: 3 }, done); + }); + + it('should define scope with default order', function (done) { + Station.reverse(function(err, stations) { + stations[0].name.should.equal('c'); + stations[0].order.should.equal(3); + stations[1].name.should.equal('b'); + stations[1].order.should.equal(2); + stations[2].name.should.equal('a'); + stations[2].order.should.equal(1); + done(); + }); + }); + + it('should override default scope order', function (done) { + Station.reverse({order: 'order ASC'}, function(err, stations) { + stations[0].name.should.equal('a'); + stations[0].order.should.equal(1); + stations[1].name.should.equal('b'); + stations[1].order.should.equal(2); + stations[2].name.should.equal('c'); + stations[2].order.should.equal(3); + done(); + }); + }); + +}); + diff --git a/test/util.test.js b/test/util.test.js index 81de4a75..f933190a 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -3,6 +3,7 @@ var utils = require('../lib/utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; var mergeSettings = utils.mergeSettings; +var sortObjectsByIds = utils.sortObjectsByIds; describe('util.fieldsToArray', function () { it('Turn objects and strings into an array of fields to include when finding models', function () { @@ -185,4 +186,35 @@ describe('mergeSettings', function () { should.deepEqual(dst.acls, expected.acls, 'Merged settings should match the expectation'); }); -}); \ No newline at end of file +}); + +describe('sortObjectsByIds', function () { + + var items = [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' }, + { id: 6, name: 'f' } + ]; + + it('should sort', function() { + var sorted = sortObjectsByIds('id', [6, 5, 4, 3, 2, 1], items); + var names = sorted.map(function(u) { return u.name; }); + should.deepEqual(names, ['f', 'e', 'd', 'c', 'b', 'a']); + }); + + it('should sort - partial ids', function() { + var sorted = sortObjectsByIds('id', [5, 3, 2], items); + var names = sorted.map(function(u) { return u.name; }); + should.deepEqual(names, ['e', 'c', 'b', 'a', 'd', 'f']); + }); + + it('should sort - strict', function() { + var sorted = sortObjectsByIds('id', [5, 3, 2], items, true); + var names = sorted.map(function(u) { return u.name; }); + should.deepEqual(names, ['e', 'c', 'b']); + }); + +});