-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[BUGFIX] Fix availability of properties in createRecord init #5413
Conversation
cc @dwickern if you could "sanity check" this change in your application that hit issue before. I've kept your test and added additional coverage. |
This should resolve #5247 as well, although not by making the API async but by ensuring that Edit We still need to wrap it in an |
I'm still on 2.x so it might be difficult to test this |
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.
Can you ensure tests exist that ensure the more common ways to instantiate a record (e.g. store.findRecord('post', 1)
et al) also passes properties to record init
?
@@ -348,6 +350,38 @@ export default class InternalModel { | |||
isError: this.isError, | |||
adapterError: this.error | |||
}; | |||
let hasCreateProperties = typeof properties === 'object' && properties !== null; |
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 would think properties !== undefined
would be fine here...
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.
It might be, depends on how defensive we want to be against things like createRecord('foo', null)
or similar. We could check undefined and then assert this check though to preserve perf.
} | ||
}); | ||
|
||
emberAssign(createOptions, properties); |
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 would prefer to avoid mutating the provided POJO (mutating for example, completely breaks using a shared POJO to .createRecord across many tests). A straightforward way to do this would be to invert this method a bit:
if (hasCreateProperties) {
let classFields = this.getFields();
for (let propertyName in properties) {
let propertyValue = properties[propertyName];
let fieldType = classFields.get(propertyName);
if (propertyName === 'id') {
this.setId(properties.id);
continue;
}
switch (fieldType) {
case 'attribute':
this.setDirtyAttribute(name, propertyValue);
break;
case 'belongsTo':
this.setDirtyBelongsTo(name, propertyValue);
initializedRelationships.push(name);
break;
case 'hasMany':
this.setDirtyHasMany(name, propertyValue);
initializedRelationships.push(name);
break;
default:
createOptions[propertyName] = propertyValue;
}
}
}
// ...snip...
getFields(attributeName) {
return get(this.modelClass, 'fields');
}
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 noticed last night that we actually have a test for using a shared pojo with createRecord and for WTF reasons it appears to still pass. I will be upgrading that test.
In regards to the proposed new direction, I like it.
@@ -358,6 +392,12 @@ export default class InternalModel { | |||
|
|||
this._record = this.store.modelFactoryFor(this.modelName).create(createOptions); | |||
|
|||
if (hasCreateProperties && initializedRelationships.length) { | |||
for (let i = 0; i < initializedRelationships.length; i++) { | |||
this._relationships.get(initializedRelationships[i]).setHasData(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.
Hmm, why doesn't setDirtyHasMany
/ setDirtyBelongsTo
mark the relationship as having data? Seems vaguely surprising to me...
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.
We can move this into the original iteration (I believe), but not into the methods. setDirtyHasMany
and setDirtyBelongsTo
are used for update code-paths as well as create-code-paths and we only mark this as hasData for creates.
this._willUpdateManyArray = true; | ||
let backburner = this.store._backburner; | ||
|
||
backburner.join(() => { |
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.
IMHO, it would be nice to remove this (and rely on the auto-run). What do you think?
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.
The problem is that too many of our APIs are sync, so we can't rely on setTimeout
(or microtasks if backburner changes) for settledness at the moment. We need the work to complete by the time someone exits the ember-data world.
This is something we would like to change, and a longterm goal of ours is to move more public APIs to being promise based to unlock optimizations around batching and updating without needing this sort of wonkiness.
…possible, skip test that should have never worked but also improve it so that we can maybe make it work in the future
426b98e
to
d578710
Compare
@@ -24,25 +24,13 @@ export default class Snapshot { | |||
this._hasManyIds = Object.create(null); | |||
this._internalModel = internalModel; | |||
|
|||
let record = internalModel.getRecord(); | |||
// TODO is there a way we can assign known attributes without |
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.
Can you make an issue so we can track fixing (or removing the TODO)?
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 left the skipped test for tracking :D
i can open an issue too though
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.
Issue is #5419
// to avoid conflicts. | ||
return this._backburner.join(() => { | ||
let normalizedModelName = normalizeModelName(modelName); | ||
let properties = copy(inputProperties) || Object.create(null); |
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.
Just notating that we will eventially need to deal with the Ember.copy
deprecation that is being worked on in emberjs/rfcs#322. Nothing needs to be done now though (AFAICT)...
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.
yeah, we know. we use it a ton :/
// using `eachAttribute`? This forces us to lookup the model-class | ||
// but for findRecord / findAll these are empty and doing so at | ||
// this point in time is unnecessary. | ||
internalModel.eachAttribute((keyName) => this._attributes[keyName] = internalModel.getAttributeValue(keyName)); |
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.
Looks like this broke defaultValue
not being considered anymore, when the attribute hasn't been accessed yet 😔
So given:
// app/models/foo.js
export default Model.extend({
bar: attr({ defaultValue: () => "baz" })
});
then
let foo = store.createRecord('foo');
// this assertion breaks after upgrading to v3.2.0-beta.2 :pensive:
assert.deepEqual(foo.toJSON(), { bar: "baz" });
Currently the whole story around defaultValue
is not ideal (e.g. #2566) and doing this in the app code by explicitly passing default values when creating a record is better, but I would still consider this a breaking change... What do you think?
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.
Seems good to open an issue for this, I think it should be fixable...
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.
Should be very fixable, surprised this broke though, re-examining my changes, will open an issue.
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.
hrm, so I didn't pull the defaultValue logic into internalModel, nor am I sure we should. https://github.com/emberjs/data/pull/5413/files#diff-c2a90350f555423721228dfa13c137f2R130
That said, the proposal in #5419 would be a good direction to address this, as we can access each attribute via the record if it exists, and only copy lazily if it does not.
This restores the changes from #4931 that were removed by #5369, ensuring that in as many cases as possible we "do the expected thing" and give access to properties in
init
.Historically, so long as
_internalModel
has data, any available record-data is available ininit
. This PR ensures that the same is true for properties passed tocreateRecord
.We achieve this by buffering the changes for dirty-state sent to
ManyArray
. In the process, we also cleanup the responsibilities ofattr
belongsTo
andhasMany
descriptors who previously were responsible for the tricky logic of where to find state and how to set state on internal-model. This should help inform and improve the APIs of emberjs/rfcs#293