Skip to content

Commit 6c6df15

Browse files
authored
Flag id as updateOnly when forceId is in effect (#1453)
* updateOnly, forceId changes * support getUpdateOnlyProperties * fixup! fix updateOrCreate in forceId mode The contract of `updateOrCreate` is expecting a full object instance to be passed to the callback. The current implementation was creating an empty instance and calling updateAttributes under the hood. As a result, the callback was called with the attributes being updated only. In order to preserve existing behaviour, we have to always build a full initial instance by calling `findById`. See the following discussion for more context: #966 * fixup! fix tests of upsert validation * move forceId to model-builder * remove TODO comment * revert refactoring and test fixes * Remove duplicate test * change testcase names * change to ModeClass.settingse * forceId setup from datasource to model-builder * fix inheritance of auto-generated forceId * Fixed failing tests for auto change * fixed a comment
1 parent aebbcd2 commit 6c6df15

7 files changed

Lines changed: 133 additions & 36 deletions

File tree

lib/dao.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -537,16 +537,11 @@ DataAccessObject.upsert = function(data, options, cb) {
537537
if (forceId) {
538538
options = Object.create(options);
539539
options.validate = !!doValidate;
540-
if (doValidate) {
541-
Model.findById(id, options, function(err, model) {
542-
if (err) return cb(err);
543-
if (!model) return cb(errorModelNotFound(id));
544-
model.updateAttributes(data, options, cb);
545-
});
546-
} else {
547-
const model = new Model({id: id}, {persisted: true});
540+
Model.findById(id, options, function(err, model) {
541+
if (err) return cb(err);
542+
if (!model) return cb(errorModelNotFound(id));
548543
model.updateAttributes(data, options, cb);
549-
}
544+
});
550545
return cb.promise;
551546
}
552547

lib/datasource.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,13 +628,6 @@ DataSource.prototype.setupDataAccess = function(modelClass, settings) {
628628
idProp.type = idType;
629629
modelClass.definition.rawProperties[idName].type = idType;
630630
modelClass.definition.properties[idName].type = idType;
631-
var forceId = settings.forceId;
632-
if (idProp.generated && forceId !== false) {
633-
forceId = true;
634-
}
635-
if (forceId) {
636-
modelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
637-
}
638631
}
639632
if (this.connector.define) {
640633
// pass control to connector

lib/model-builder.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,32 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
342342
});
343343
}
344344

345+
// updateOnly property is added to indicate that this property will appear in
346+
// the model for update/updateorcreate operations but and not for create operation.
347+
var forceId = ModelClass.settings.forceId;
348+
if (idNames.length > 0) {
349+
var idName = modelDefinition.idName();
350+
idProp = ModelClass.definition.rawProperties[idName];
351+
if (idProp.generated && forceId !== false) {
352+
forceId = 'auto';
353+
} else if (!idProp.generated && forceId === 'auto') {
354+
// One of our parent models has enabled forceId because
355+
// it uses an auto-generated id property. However,
356+
// this particular model does not use auto-generated id,
357+
// therefore we need to disable `forceId`.
358+
forceId = false;
359+
}
360+
361+
if (forceId) {
362+
ModelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
363+
}
364+
365+
ModelClass.definition.properties[idName].updateOnly = !!forceId;
366+
ModelClass.definition.rawProperties[idName].updateOnly = !!forceId;
367+
368+
ModelClass.settings.forceId = forceId;
369+
}
370+
345371
// A function to loop through the properties
346372
ModelClass.forEachProperty = function(cb) {
347373
var props = ModelClass.definition.properties;

lib/model.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,18 @@ ModelBaseClass.getMergePolicy = function(options) {
827827
return mergePolicy;
828828
};
829829

830+
/**
831+
* Gets properties defined with 'updateOnly' flag set to true from the model. This flag is also set to true
832+
* internally for the id property, if this property is generated and IdInjection is true.
833+
* @returns {updateOnlyProps} List of properties with updateOnly set to true.
834+
*/
835+
836+
ModelBaseClass.getUpdateOnlyProperties = function() {
837+
var Model = this;
838+
const props = this.definition.properties;
839+
return Object.keys(props).filter(key => props[key].updateOnly);
840+
};
841+
830842
// Mixin observer
831843
jutil.mixin(ModelBaseClass, require('./observer'));
832844

test/loopback-dl.test.js

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ describe('DataSource define model', function() {
645645

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

650650
done();
651651
});
@@ -663,13 +663,13 @@ describe('DataSource define model', function() {
663663

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

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

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

674674
done();
675675
});
@@ -1911,3 +1911,61 @@ describe('ModelBuilder options.models', function() {
19111911
assert.deepEqual(codes.number, ['unknown-property']);
19121912
});
19131913
});
1914+
1915+
describe('updateOnly', function() {
1916+
it('sets forceId to true when model id is generated', function(done) {
1917+
var ds = new DataSource('memory');
1918+
var Post = ds.define('Post', {
1919+
title: {type: String, length: 255},
1920+
date: {type: Date, default: function() {
1921+
return new Date();
1922+
}},
1923+
});
1924+
// check if forceId is added as true in ModelClass's settings[] explicitly,
1925+
// if id a generated (default) and forceId in from the model is
1926+
// true(unspecified is 'true' which is the default).
1927+
Post.settings.should.have.property('forceId').eql('auto');
1928+
done();
1929+
});
1930+
1931+
it('flags id as updateOnly when forceId is undefined', function(done) {
1932+
var ds = new DataSource('memory');
1933+
var Post = ds.define('Post', {
1934+
title: {type: String, length: 255},
1935+
date: {type: Date, default: function() {
1936+
return new Date();
1937+
}},
1938+
});
1939+
// check if method getUpdateOnlyProperties exist in ModelClass and check if
1940+
// the Post has 'id' in updateOnlyProperties list
1941+
Post.should.have.property('getUpdateOnlyProperties');
1942+
Post.getUpdateOnlyProperties().should.eql(['id']);
1943+
done();
1944+
});
1945+
1946+
it('does not flag id as updateOnly when forceId is false', function(done) {
1947+
var ds = new DataSource('memory');
1948+
var Person = ds.define('Person', {
1949+
name: String,
1950+
gender: String,
1951+
}, {forceId: false});
1952+
// id should not be there in updateOnly properties list if forceId is set
1953+
// to false
1954+
Person.should.have.property('getUpdateOnlyProperties');
1955+
Person.getUpdateOnlyProperties().should.eql([]);
1956+
done();
1957+
});
1958+
1959+
it('flags id as updateOnly when forceId is true', function(done) {
1960+
var ds = new DataSource('memory');
1961+
var Person = ds.define('Person', {
1962+
name: String,
1963+
gender: String,
1964+
}, {forceId: true});
1965+
// id should be there in updateOnly properties list if forceId is set
1966+
// to true
1967+
Person.should.have.property('getUpdateOnlyProperties');
1968+
Person.getUpdateOnlyProperties().should.eql(['id']);
1969+
done();
1970+
});
1971+
});

test/model-inheritance.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ describe('Model class inheritance', function() {
239239
);
240240

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

259261
assert.deepEqual(Customer.settings, {
262+
forceId: false,
260263
defaultPermission: 'DENY',
261264
acls: [
262265
{

test/validations.test.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -205,36 +205,46 @@ describe('validations', function() {
205205
it('should ignore errors on upsert by default', function(done) {
206206
delete User.validations;
207207
User.validatesPresenceOf('name');
208-
// It's important to pass an id value, otherwise DAO falls back
209-
// to regular create()
210-
User.updateOrCreate({id: 999}, done);
208+
// It's important to pass an existing id value to updateOrCreate,
209+
// otherwise DAO falls back to regular create()
210+
User.create({name: 'a-name'}, (err, u) => {
211+
if (err) return done(err);
212+
User.updateOrCreate({id: u.id}, done);
213+
});
211214
});
212215

213216
it('should be skipped by upsert when disabled via settings', function(done) {
214217
var Customer = User.extend('Customer');
215218
Customer.attachTo(db);
216219
db.autoupdate(function(err) {
217220
if (err) return done(err);
218-
Customer.prototype.isValid = function() {
219-
throw new Error('isValid() should not be called at all');
220-
};
221-
Customer.settings.validateUpsert = false;
222-
// It's important to pass an id value, otherwise DAO falls back
223-
// to regular create()
224-
Customer.updateOrCreate({id: 999}, done);
221+
// It's important to pass an existing id value,
222+
// otherwise DAO falls back to regular create()
223+
Customer.create({name: 'a-name'}, (err, u) => {
224+
if (err) return done(err);
225+
226+
Customer.prototype.isValid = function() {
227+
throw new Error('isValid() should not be called at all');
228+
};
229+
Customer.settings.validateUpsert = false;
230+
231+
Customer.updateOrCreate({id: u.id, name: ''}, done);
232+
});
225233
});
226234
});
227235

228236
it('should work on upsert when enabled via settings', function(done) {
229-
delete User.validations;
230237
User.validatesPresenceOf('name');
231238
User.settings.validateUpsert = true;
232-
// It's important to pass an id value, otherwise DAO falls back
233-
// to regular create()
234-
User.upsert({id: 999}, function(err, u) {
235-
if (!err) return done(new Error('Validation should have failed.'));
236-
err.should.be.instanceOf(ValidationError);
237-
done();
239+
// It's important to pass an existing id value,
240+
// otherwise DAO falls back to regular create()
241+
User.create({name: 'a-name'}, (err, u) => {
242+
if (err) return done(err);
243+
User.upsert({id: u.id, name: ''}, function(err, u) {
244+
if (!err) return done(new Error('Validation should have failed.'));
245+
err.should.be.instanceOf(ValidationError);
246+
done();
247+
});
238248
});
239249
});
240250

0 commit comments

Comments
 (0)