Doing something outside of typical CRUD


#1

I’ve posted about this before on various forums as I’ve migrated my app from earlier ember-data 1.x days up to current 2.x stuff. I’ve gotten it to work with various hacks, but each time I go through a migration of ember-data I try and see if I can get it done in a cleaner fashion.

Here’s the scenario:

  • The API I’m dealing with is not very REST-compliant.
  • The API has a single end-point for save new vs. update (both using POST as the http verb) [incidentally, it determines which is intended based on the primary key of the payload being prefixed with “new” or not…ugly I know]

Now, while the API is kind of crap in those senses, it does have a rather cool feature that it calls auto-updating of transient objects. The concept is that while the user is constructing an object on the client, the server offers the ability to ship the transient object to the server wherein the server will validate in real time and potentially add or update other data elements on your object for you in response to user input. For example, think of a sales order where you add a new product to your cart. You can ship your cart to the server as-is with a special call that will validate the cart for you as well as recalculate sales tax via whatever rules it uses or perhaps it activates a volume discount automatically and adds that to the cart because the additional product crosses a minimum threshold. Or maybe the item you just added can’t be added to your cart for some reason, then it will return an error telling you why on that item. The point is that the user gets immediate feedback on those things as well as having the object validated…but without persisting the object on the server.

The server does this via a special end-point /objectName/get_updates (as opposed to /save when you’re ready to persist the entity).

From Ember’s point-of-view though, it’s really not super different from createRecord or updateRecord (except that in the end, the state of the object really shouldn’t be saved to be accurate). Yet I still need the ability to attach errors to fields and stuff like that.

Here’s my latest iteration of how I’m doing this and would love feedback on perhaps a more idiomatically Ember way of doing this as this still seems rather hacky to me:

  1. I created an initializer to re-open the base model class and added:

    initialize: function(/* container, app */) { DS.Model.reopen({ //see base save function for InternalModel autoUpdate: function(options) { var promiseLabel = "DS: Model#autoUpdate " + this; var resolver = Ember.RSVP.defer(promiseLabel);

     this.store.scheduleAutoUpdate(this.model._internalModel, resolver, options);
     return resolver.promise;
    

    } }); }

  2. Then I added analogs to scheduleSave and flushPendingSave on an initializer that re-opens the store:

    scheduleAutoUpdate: function(internalModel, resolver, options) { var snapshot = internalModel.createSnapshot(options); internalModel.flushChangedAttributes(); internalModel.adapterWillCommit();

     this.autoUpdateContext = {
       snapshot: snapshot,
       resolver: resolver
     };
     
     once(this, 'flushPendingAutoUpdate');
    

    },

    //see store.flushPendingSave flushPendingAutoUpdate: function() { var pendingUpdate = this.autoUpdateContext; this.autoUpdateContext = {};

     var snapshot = pendingUpdate.snapshot;
     var resolver = pendingUpdate.resolver;
     var record = snapshot._internalModel;
     if(record.isLoaded() === false || record.hasDirtyAttributes() === false) {
       return;
     }
     var adapter = this.adapterFor(record.type.modelName);
    
     resolver.resolve(_commit(adapter, this, 'autoupdateRecord', snapshot));
    

    },

I then have to copy the _commit function from the store since it’s not exposed (maybe there’s a way to get at it but I couldn’t figure it out if there is). I’ve also added the appropriate buildurl and operation specific functions on the adapter to handle this.

It seems like if I just had a way to control which operation was chosen as opposed to the base flushPendingSave automatically doing so based on the record’s current state I’d be golden.

Is there a better way to do this or a way that ember-data could be abstracted a bit more in this area to allow me to customize the behavior without having to do so much copy & paste?


#2

Oh another thing here… copying _commit to this initializer is a royal PITA because the Ember folks don’t want you messing with it…so they stick in a scope that I can’t figure out how to access from an initializer. Worse, when you copy the method here, it itself calls a bunch of little helper functions that are similarly obfuscated (on purpose I realize) so you end up having to copy all of those. This is really, really ugly. I’d be pretty happy if someone could point me to a way to avoid doing all of that as well.


#3

I’m far from an expert but I’ve done a bit of this kind of customization. Honestly no matter how hard you try and adhere to REST conventions there’s always something which doesn’t quite fit. I haven’t found any case where I’ve needed to do anything besides create custom Serializers and Adapters though, so your strategy of reopening Models and the Store and copying a bunch of Ember Data internals feels like unnecessary surgery.

These are the two articles that really made things click for me in terms of understanding how Ember wants you to handle this pretty common requirement:

Custom adapters and serializers

Working with custom api endpoints


#4

Thanks. It feels wrong, I agree. I’ve seen the emberigniter article and it was helpful in hooking up to our goofy API. Essentially just needed a custom normalizeResponse to munge our response into a JSON-API looking thing.

I had not seen the other one suggesting an Ember CLI add-on to deal with custom end-points. I’ll definitely look at that one. I think that might solve some of my problems, but not sure it covers everything I need yet. I’ll report back after looking into that.


#5

It appears that the custom api endpoints add-on will indeed help me not have to copy a bunch of low-level store code.

I still think though that I’ve got a goofy use-case here. I’m doing something semantically close to a “save” on a loaded.created.uncommitted record and I want the response to that action to be able to set / overwrite properties on that record as well as set an invalid state and set errors on individual fields while somehow retaining the fact after all that magic is done that it is still an uncommitted record. To top it off, I need to do the same behavior with loaded.updated.uncommitted too… I have a feeling that I’m going to butt up against the state machine for records…but maybe not.


#6

Yea it sure looks like you have an odd case that requires some involved custom logic. The question is where does that code live for easy grokking, testing, and maintaining. It feels like a custom method(s) on the Model (and maybe some properties to track additional state) and a custom Adapter would corral the goofiness appropriately though.


#7

Agreed. I’ll keep hacking. I should probably post somewhere when I’ve got it figured out just in case someone else has to bang their head on similar nonsense :smile: