-
Notifications
You must be signed in to change notification settings - Fork 362
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start! Please take a look at my comments below.
lib/model-builder.js
Outdated
ModelClass.definition.defineProperty('id', {type: Number, id: 1, generated: true}); | ||
// updateOnly property is added to indicate that this property will appear in the model for update/updateorcreate | ||
// operations but and not for create operation. | ||
ModelClass.definition.defineProperty('id', {type: Number, id: 1, generated: true, updateOnly: true}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think updateOnly
should be set only when forceId
is in effect. With forceId:false
, clients can provide custom id
value even when the id
column was injected by the datasource.
I think updateOnly
flag should be set in lib/datasource.js
:
if (forceId) {
modelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
// set updateOnly here
// something along the following lines, I am not sure if we need to change rawProperties too
modelClass.definition.properties[idName].updateOnly = true;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos We can't set updateOnly property in the above place, since LoopBack repository's PersistentModel.setupRemoting() --> getUpdateOnlyProperties() gets invoked before above logic in datasource-gluggler's datasource.js code gets executed. Since getUpdateOnlyProperties() gets invoked before setting 'updateOnly' model as you suggested above, getUpdateOnlyProperties() will not return id property even if forceId is true. Any other parts of the logic we can set this property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos As alternate approach, I tried this in model builder
// Add the id property
if (idInjection) {
// Set up the id property
// updateOnly property is added to indicate that this property will appear in the model for update related operations
// but and not for create operation.
if (ModelClass.definition.settings.forceId !== false) {
ModelClass.definition.defineProperty('id', {type: Number, id: 1, generated: true, updateOnly: true});
} else {
ModelClass.definition.defineProperty('id', {type: Number, id: 1, generated: true});
}
}
}
Unfortunately, this doesn't seem to work either if user explicitly sets forceId = false in model file, for e.g book.json file. ModelClass.definition.settings.forceId logic seems to look at the base model's (Model) setting rather than the book model's setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this doesn't seem to work either if user explicitly sets forceId = false in model file, for e.g book.json file. ModelClass.definition.settings.forceId logic seems to look at the base model's (Model) setting rather than the book model's setting.
I think you should be using ModelClass.settings.forceId
here.
There are two things I am concerned about:
- We should keep the logic deciding how to treat
forceId: undefined
in a single place. - When
forceId
is effectivelyfalse
, then theid
property should not be marked asupdateOnly
.
I did not realize that setupDataAccess
is called too late in our setup process. In that case, I am proposing to move all logic determining forceId
from setupDataAccess
to this method (ModelBuilder.prototype.define
). I am talking about the following lines:
if (idProp.generated && forceId !== false) {
forceId = true;
}
// set the calculated forceId back to settings
settings.forceId = forceId;
I believe we should have all required information already available here. The only thing that can change in setupDataAccess
is the type of the id
property, but that is not affecting forceId
(at least I think so).
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree ^^
I tried
var forceId = ModelClass.settings.forceId;
if (idNames.length > 0) {
// id already exists
idInjection = false;
// Does method `idName` also works for composite Id at this phase?
var idName = modelDefinition.idName();
var idProp = ModelClass.definition.rawProperties[idName];
if (idProp.generated && forceId !== false) {
forceId = true;
}
} else {
if (forceId !== false) forceId = true;
}
ModelClass.settings.forceId = forceId;
received same errors as @rashmihunt is fixing in this PR, other tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos what you are proposing is to move only the forceId determining logic from setUpDataAccess and leave rest of the logic as is.. for e.g setting idType and calling modelClass.validatesAbsenceOf(..) based on
forceId from the settings.
DataSource.prototype.setupDataAccess = function(modelClass, settings) {
if (this.connector) {
// Check if the id property should be generated
var idName = modelClass.definition.idName();
var idProp = modelClass.definition.rawProperties[idName];
if (idProp && idProp.generated && this.connector.getDefaultIdType) {
// Set the default id type from connector's ability
var idType = this.connector.getDefaultIdType() || String;
idProp.type = idType;
modelClass.definition.rawProperties[idName].type = idType;
modelClass.definition.properties[idName].type = idType;
var forceId = settings.forceId;
if (forceId) {
modelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'});
}
}
....
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos I did try your proposed solution and it does work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what you are proposing is to move only the forceId determining logic from setUpDataAccess and leave rest of the logic as is.. for e.g setting idType and calling modelClass.validatesAbsenceOf(..) based on
forceId from the settings.
I did try your proposed solution and it does work.
Cool!
I think it probably makes sense to move modelClass.validatesAbsenceOf
over too, while we are at cleaning up this code. Having said that, what you are proposing in your comment above looks reasonable too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos I saw lot of tests failed in CI in downstream component 'loopback' if calculated forceId is set back into model class's settings[] in mode-builder.define (..) method instead of datasource.setupDataAccess(). See in this commit
bb9be2d#diff-546e03515f85748fe5b4d0db11cd81b7R355
I am bit puzzled what's the difference between setting it in model-builder.js vs datasource.js.
To avoid the CI failures, I reverted the refactoring of model-builder.js and datasource.js with my latest commit. PTAL.
lib/model.js
Outdated
var property; | ||
var updateOnlyProps = []; | ||
|
||
for (var key in props) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use Array.prototype.filter to simplify this code.
const props = this.definition.properties;
return Object.key(props).filter(key => props[key].updateOnly);
test/loopback-dl.test.js
Outdated
|
||
// check if method getUpdateOnlyProperties exist in ModelClass and check if the Post has 'id' in updateOnlyProperties list |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One assert per test please. There should be two tests: one checking Post.settings.forceId
, the other verifying getUpdateOnlyProperties()
. We may want to write more tests for getUpdateOnlyProperties
in the future, it's probably a good idea to create a new describe
block for tests of this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos done
test/loopback-dl.test.js
Outdated
// check if method getUpdateOnlyProperties exist in ModelClass and check if the Post has 'id' in updateOnlyProperties list | ||
Post.should.have.property('getUpdateOnlyProperties'); | ||
var updateOnlyProps = Post.getUpdateOnlyProperties(); | ||
updateOnlyProps.should.containEql('id'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in this test we know exactly what properties should be marked as updateOnly
, don't we? In which case I think it's better to assert the exact content of the returned array:
updateOnlyProps.should.eql(['id']);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos done PTAL
test/default-scope.test.js
Outdated
@@ -208,7 +208,7 @@ describe('default scope', function() { | |||
var data = {id: ids.productA, description: 'Anything...', kind: 'ingored'}; | |||
Tool.updateOrCreate(data, function(err, p) { | |||
should.not.exist(err); | |||
p.name.should.equal('Product A'); | |||
// p.name.should.equal('Product A'); // TODO [rashmi] Sync up with Miroslav about this. propertyly setting the forceId back to settings has this side affect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the side effects? Could you post the outcome of the test here please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please ignore my comment, I see you have already analyzed the issue and posted learning in the pull request description 👍
@rashmihunt I pushed two commits to fix the test failures you have discovered - see d6716ae and 023439b |
I am cross-posting the commit message from d6716ae for posterity: The contract of 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 See the following discussion for more context: #966 |
I feel |
Yes, I was keeping the use case of @rashmihunt would you mind splitting this pull request into two - one fixing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rashmihunt The changes look reasonable to me, just one same comment as Miroslav posts in https://github.com/strongloop/loopback-datasource-juggler/pull/1453/files#r131837163, I did a quick try, seems like a good solution.
@slnode test please |
9b11f61
to
927e209
Compare
Cloudant fails with same reason in loopbackio/loopback-connector-cloudant#161 (comment) There is a big refactor happen in that repo and two PRs block each other, so I have to check in one(with the failure) then the other one(contains the fix), it's not caused by this PR. |
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
927e209
to
f012c09
Compare
@slnode test please |
} | ||
|
||
if (forceId) { | ||
ModelClass.validatesAbsenceOf(idName, {if: 'isNewRecord'}); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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)
@bajtos Please take a look. I will sync up with you on the test regressions.
Other PRs involved in the solution of issue strongloop/loopback#2924
Changes in this PR
In dao.js
if
forceId
is true in most failing test cases anddoValidate
is false, which means it ends up inwhich creates a new model and with the new set of attributes passed in. In one case
validateUpsert
is calculated and set into 'doValidate' in logic prior to above block of code. Looking through the documentation and closed issues in 3.x , I noticed that in 3.x, validateUpsert is changed to 'true' by default, which means 'undefined' should be considered as 'true' ? Should there be logic just before -if (forceId) {..}
Anyway, I will sync up with you on Monday morning on this.