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

[BUGFIX] Fix availability of properties in createRecord init #5413

Merged
merged 12 commits into from Apr 4, 2018

Conversation

runspired
Copy link
Contributor

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 in init. This PR ensures that the same is true for properties passed to createRecord.

We achieve this by buffering the changes for dirty-state sent to ManyArray. In the process, we also cleanup the responsibilities of attr belongsTo and hasMany 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

@runspired
Copy link
Contributor Author

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.

@runspired
Copy link
Contributor Author

runspired commented Apr 3, 2018

This should resolve #5247 as well, although not by making the API async but by ensuring that createRecord joins ember-data's internal backburner loop to setup relationships.

Edit We still need to wrap it in an Ember.run as well. We previously agreed to do so, I'll follow this PR up with one that does and cleans up test code that wraps createRecord in run loops.

@runspired runspired changed the title [BUFXIF] Fix availability of properties in createRecord init [BUGFIX] Fix availability of properties in createRecord init Apr 3, 2018
@dwickern
Copy link
Contributor

dwickern commented Apr 4, 2018

I'm still on 2.x so it might be difficult to test this

@mmun mmun assigned mmun and unassigned mmun Apr 4, 2018
Copy link
Member

@rwjblue rwjblue left a 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;
Copy link
Member

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...

Copy link
Contributor Author

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);
Copy link
Member

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');
}

Copy link
Contributor Author

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);
Copy link
Member

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...

Copy link
Contributor Author

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(() => {
Copy link
Member

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?

Copy link
Contributor Author

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.

@@ -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
Copy link
Member

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)?

Copy link
Contributor Author

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

Copy link
Contributor Author

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);
Copy link
Member

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)...

Copy link
Contributor Author

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 :/

@runspired runspired merged commit 7b5a885 into master Apr 4, 2018
@runspired runspired deleted the fix-create-record-init branch April 4, 2018 23:14
// 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));
Copy link
Member

@pangratz pangratz Apr 9, 2018

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?

Copy link
Member

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...

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants