Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flag id as updateOnly when forceId is in effect #1453

Merged
merged 14 commits into from
Aug 22, 2017
13 changes: 4 additions & 9 deletions lib/dao.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,16 +537,11 @@ DataAccessObject.upsert = function(data, options, cb) {
if (forceId) {
options = Object.create(options);
options.validate = !!doValidate;
if (doValidate) {
Model.findById(id, options, function(err, model) {
if (err) return cb(err);
if (!model) return cb(errorModelNotFound(id));
model.updateAttributes(data, options, cb);
});
} else {
const model = new Model({id: id}, {persisted: true});
Model.findById(id, options, function(err, model) {
if (err) return cb(err);
if (!model) return cb(errorModelNotFound(id));
model.updateAttributes(data, options, cb);
}
});
return cb.promise;
}

Expand Down
7 changes: 0 additions & 7 deletions lib/datasource.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,13 +628,6 @@ DataSource.prototype.setupDataAccess = function(modelClass, settings) {
idProp.type = idType;
modelClass.definition.rawProperties[idName].type = idType;
modelClass.definition.properties[idName].type = idType;
var forceId = settings.forceId;
if (idProp.generated && forceId !== false) {
forceId = true;
}
if (forceId) {
modelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
}
}
if (this.connector.define) {
// pass control to connector
Expand Down
26 changes: 26 additions & 0 deletions lib/model-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,32 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
});
}

// updateOnly property is added to indicate that this property will appear in
// the model for update/updateorcreate operations but and not for create operation.
var forceId = ModelClass.settings.forceId;
if (idNames.length > 0) {
var idName = modelDefinition.idName();
idProp = ModelClass.definition.rawProperties[idName];
if (idProp.generated && forceId !== false) {
forceId = 'auto';
} else if (!idProp.generated && forceId === 'auto') {
// One of our parent models has enabled forceId because
// it uses an auto-generated id property. However,
// this particular model does not use auto-generated id,
// therefore we need to disable `forceId`.
forceId = false;
}

if (forceId) {
ModelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rashmihunt @bajtos shouldn't we also reject any requests containing an empty string ID when forceId is true? validatesAbsenceOf considers an empty string value valid, which generates a record with no id (at least with MongoDB) when calling a POST endpoint with an explicit empty string ID even if forceId is true (I'm new to LoopBack, so I might have missed something).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For posterity, I responded here: #1519 (comment)

}

ModelClass.definition.properties[idName].updateOnly = !!forceId;
ModelClass.definition.rawProperties[idName].updateOnly = !!forceId;

ModelClass.settings.forceId = forceId;
}

// A function to loop through the properties
ModelClass.forEachProperty = function(cb) {
var props = ModelClass.definition.properties;
Expand Down
12 changes: 12 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,18 @@ ModelBaseClass.getMergePolicy = function(options) {
return mergePolicy;
};

/**
* Gets properties defined with 'updateOnly' flag set to true from the model. This flag is also set to true
* internally for the id property, if this property is generated and IdInjection is true.
* @returns {updateOnlyProps} List of properties with updateOnly set to true.
*/

ModelBaseClass.getUpdateOnlyProperties = function() {
var Model = this;
const props = this.definition.properties;
return Object.keys(props).filter(key => props[key].updateOnly);
};

// Mixin observer
jutil.mixin(ModelBaseClass, require('./observer'));

Expand Down
64 changes: 61 additions & 3 deletions test/loopback-dl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ describe('DataSource define model', function() {

var User = ds.define('User', {});
assert.deepEqual(User.definition.properties.id,
{type: Number, id: 1, generated: true});
{type: Number, id: 1, generated: true, updateOnly: true});

done();
});
Expand All @@ -663,13 +663,13 @@ describe('DataSource define model', function() {

var User = builder.define('User', {id: {type: String, generated: true, id: true}});
assert.deepEqual(User.definition.properties.id,
{type: String, id: 1, generated: true});
{type: String, id: 1, generated: true, updateOnly: true});

var ds = new DataSource('memory');// define models
User.attachTo(ds);

assert.deepEqual(User.definition.properties.id,
{type: Number, id: 1, generated: true});
{type: Number, id: 1, generated: true, updateOnly: true});

done();
});
Expand Down Expand Up @@ -1911,3 +1911,61 @@ describe('ModelBuilder options.models', function() {
assert.deepEqual(codes.number, ['unknown-property']);
});
});

describe('updateOnly', function() {
it('sets forceId to true when model id is generated', function(done) {
var ds = new DataSource('memory');
var Post = ds.define('Post', {
title: {type: String, length: 255},
date: {type: Date, default: function() {
return new Date();
}},
});
// check if forceId is added as true in ModelClass's settings[] explicitly,
// if id a generated (default) and forceId in from the model is
// true(unspecified is 'true' which is the default).
Post.settings.should.have.property('forceId').eql('auto');
done();
});

it('flags id as updateOnly when forceId is undefined', function(done) {
var ds = new DataSource('memory');
var Post = ds.define('Post', {
title: {type: String, length: 255},
date: {type: Date, default: function() {
return new Date();
}},
});
// check if method getUpdateOnlyProperties exist in ModelClass and check if
// the Post has 'id' in updateOnlyProperties list
Post.should.have.property('getUpdateOnlyProperties');
Post.getUpdateOnlyProperties().should.eql(['id']);
done();
});

it('does not flag id as updateOnly when forceId is false', function(done) {
var ds = new DataSource('memory');
var Person = ds.define('Person', {
name: String,
gender: String,
}, {forceId: false});
// id should not be there in updateOnly properties list if forceId is set
// to false
Person.should.have.property('getUpdateOnlyProperties');
Person.getUpdateOnlyProperties().should.eql([]);
done();
});

it('flags id as updateOnly when forceId is true', function(done) {
var ds = new DataSource('memory');
var Person = ds.define('Person', {
name: String,
gender: String,
}, {forceId: true});
// id should be there in updateOnly properties list if forceId is set
// to true
Person.should.have.property('getUpdateOnlyProperties');
Person.getUpdateOnlyProperties().should.eql(['id']);
done();
});
});
3 changes: 3 additions & 0 deletions test/model-inheritance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ describe('Model class inheritance', function() {
);

assert.deepEqual(User.settings, {
// forceId is set to 'auto' in memory if idProp.generated && forceId !== false
forceId: 'auto',
defaultPermission: 'ALLOW',
acls: [
{
Expand All @@ -257,6 +259,7 @@ describe('Model class inheritance', function() {
});

assert.deepEqual(Customer.settings, {
forceId: false,
defaultPermission: 'DENY',
acls: [
{
Expand Down
44 changes: 27 additions & 17 deletions test/validations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,36 +205,46 @@ describe('validations', function() {
it('should ignore errors on upsert by default', function(done) {
delete User.validations;
User.validatesPresenceOf('name');
// It's important to pass an id value, otherwise DAO falls back
// to regular create()
User.updateOrCreate({id: 999}, done);
// It's important to pass an existing id value to updateOrCreate,
// otherwise DAO falls back to regular create()
User.create({name: 'a-name'}, (err, u) => {
if (err) return done(err);
User.updateOrCreate({id: u.id}, done);
});
});

it('should be skipped by upsert when disabled via settings', function(done) {
var Customer = User.extend('Customer');
Customer.attachTo(db);
db.autoupdate(function(err) {
if (err) return done(err);
Customer.prototype.isValid = function() {
throw new Error('isValid() should not be called at all');
};
Customer.settings.validateUpsert = false;
// It's important to pass an id value, otherwise DAO falls back
// to regular create()
Customer.updateOrCreate({id: 999}, done);
// It's important to pass an existing id value,
// otherwise DAO falls back to regular create()
Customer.create({name: 'a-name'}, (err, u) => {
if (err) return done(err);

Customer.prototype.isValid = function() {
throw new Error('isValid() should not be called at all');
};
Customer.settings.validateUpsert = false;

Customer.updateOrCreate({id: u.id, name: ''}, done);
});
});
});

it('should work on upsert when enabled via settings', function(done) {
delete User.validations;
User.validatesPresenceOf('name');
User.settings.validateUpsert = true;
// It's important to pass an id value, otherwise DAO falls back
// to regular create()
User.upsert({id: 999}, function(err, u) {
if (!err) return done(new Error('Validation should have failed.'));
err.should.be.instanceOf(ValidationError);
done();
// It's important to pass an existing id value,
// otherwise DAO falls back to regular create()
User.create({name: 'a-name'}, (err, u) => {
if (err) return done(err);
User.upsert({id: u.id, name: ''}, function(err, u) {
if (!err) return done(new Error('Validation should have failed.'));
err.should.be.instanceOf(ValidationError);
done();
});
});
});

Expand Down