ember-data-model-fragments: Model fragments broken against Ember Data 3.28

I was testing Ember Data Model Fragments against the latest Ember Data - version 3.28 - and unfortunately it looks like they’re not compatible. Here’s what happens when I try to save a model that contains a fragment: Screen Shot 2021-08-26 at 7 30 55 am

index.js:178 Uncaught Error: Assertion Failed: You need to pass a model name to the store's serializerFor method
    at assert (index.js:178)
    at Store.serializerFor (ext.js:230)
    at Store.superWrapper [as serializerFor] (index.js:442)
    at Class.serializeFragment [as serialize] (fragment.js:40)
    at ApplicationSerializer.serializeAttribute (json.js:1105)
    at json.js:1035
    at -private.js:1611
    at Array.forEach (<anonymous>)
    at Snapshot.eachAttribute (-private.js:1610)
    at ApplicationSerializer.serialize (json.js:1034)
assert @ index.js:178
serializerFor @ ext.js:230
superWrapper @ index.js:442
serializeFragment @ fragment.js:40
serializeAttribute @ json.js:1105
(anonymous) @ json.js:1035
(anonymous) @ -private.js:1611
eachAttribute @ -private.js:1610
serialize @ json.js:1034
serialize @ rest.js:584
superWrapper @ index.js:442
serializeIntoHash @ rest.js:612
serializeIntoHash @ -private.js:634
createRecord @ rest.js:694
(anonymous) @ -private.js:1867
invokeCallback @ rsvp.js:493
(anonymous) @ rsvp.js:558
(anonymous) @ rsvp.js:19
invoke @ backburner.js:338
flush @ backburner.js:229
flush @ backburner.js:426
_end @ backburner.js:958
end @ backburner.js:708
_run @ backburner.js:1013
run @ backburner.js:752
run @ index.js:116
callback @ application.js:434

This particular problem seems to happen at https://github.com/adopted-ember-addons/ember-data-model-fragments/blob/b1a661b3a8c07bd9d395769498062db8871d25f9/addon/transforms/fragment.js#L38 The serialize method is expecting a Snapshot, but actually receives the model instance of the fragment (I’m not sure why) - modelName is empty there.

Here’s a minimal reproduction on a fresh Ember app: ember-quickstart.zip

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 7
  • Comments: 27 (8 by maintainers)

Commits related to this issue

Most upvoted comments

Here’s a quick workaround,

import FragmentTransform from 'ember-data-model-fragments/transforms/fragment';
import FragmentArrayTransform from 'ember-data-model-fragments/transforms/fragment-array';

FragmentTransform.reopen({
  serialize(snapshot) {
    return this._super(snapshot?._createSnapshot?.() ?? snapshot);
  },
});

FragmentArrayTransform.reopen({
  serialize(snapshots) {
    return this._super(snapshots?._createSnapshot?.() ?? snapshots);
  },
});

As @lowski pointed out, the problem may be that before 3.28 the Snapshot for serialization is created by calling InternalModel.createSnapshot which is overridden in ext.js to recursively transform Fragments to Snapshots. In 3.28 the Snapshot for serialization is created by calling new Snapshot() directly instead so Fragments are no longer transformed to Snapshots.

I was able to dirty fix it by calling createSnapshot manually in the FragmentTransform.serialize method :

// transforms/fragment.js

serialize: function serializeFragment(snapshot) {
  if (!snapshot) {
    return null;
  }

  // Dirty fix for Ember Data 3.28
  // snapshot may be a Fragment instead of a Snapshot
  if (typeof snapshot._createSnapshot === 'function') {
    snapshot = snapshot._createSnapshot();
  }

  let store = this.store;
  let serializer = store.serializerFor(snapshot.modelName);

  return serializer.serialize(snapshot);
},
// transforms/fragment-array.js

serialize: function serializeFragmentArray(snapshots) {
  if (!snapshots) {
    return null;
  }

  // Dirty fix for Ember Data 3.28
  // snapshots may be a Fragment instead of an Array<Snapshot>
  if (typeof snapshots._createSnapshot === 'function') {
    snapshots = snapshots._createSnapshot();
  }

  let store = this.store;

  return snapshots.map(snapshot => {
    let serializer = store.serializerFor(snapshot.modelName);
    return serializer.serialize(snapshot);
  });
}

There is probably a better way / place to do this.

The right way to do this is to store the serialized form in the fragment RecordData, no private apis would be necessary then.

@knownasilya @kevinkucharczyk I believe found the culprit: CoreStore in 3.27.1 doesn’t have REQUEST_SERVICE flag enabled (in 3.28.1 it’s already on) so scheduleSave relies on pendingSave queue (which uses internalModel.createSnapshot(options) to create model’s snapshots):

let snapshot = internalModel.createSnapshot(options);
this._pendingSave.push({
  snapshot: snapshot,
  resolver: resolver,
});

emberBackburner.scheduleOnce('actions', this, this.flushPendingSave);

In 3.28.1 scheduleSave goes through fetchManager (via if (REQUEST_SERVICE) { ... } conditional) and it uses Snapshot constructor to build model’s snapshot:

let snapshot = new Snapshot(options, identifier, this._store);

For some reason, fragments are not correctly converted to snapshots in this case. I don’t know if it’s bug or desired behavior.

I’m not an Ember expert by any means so I’m not sure what’s the most appropriate solution but in 3.28.1 serializer.serialize(snapshot) operates on Fragment instead of Snapshot. I don’t know what consequences it might have 🤷 .

I merged that fix, so master should work as well. Will try to research a bit more and see if there is a better solution.

@knownasilya I’ve just tested it on my project and #407 doesn’t fix this issue - now I’m getting a following error:

Error: Assertion Failed: The `attr` method is not available on Model, a Snapshot was probably expected. Are you passing a Model instead of a Snapshot to your serializer?

@kevinkucharczyk can you check latest master on your project as well?

I can confirm this. I just bumped my app to 3.28.0 and was bitten by the same error.