Skip to content

Instantly share code, notes, and snippets.

@sheldonbaker
Last active February 20, 2017 11:07
Show Gist options
  • Save sheldonbaker/7330288 to your computer and use it in GitHub Desktop.
Save sheldonbaker/7330288 to your computer and use it in GitHub Desktop.
A model and a couple adapters to enable batch requests against a rails-api server implementing https://github.com/arsduo/batch_api. Uses Ember.RSVP.defer() extensively to allow a BatchAdapter to call `save` against multiple records and fire off a single AJAX request.
batch = store.createRecord('batch')
foo1 = store.createRecord 'foo',
title: 'I am a foo'
foo2 = store.createRecord 'foo',
title: 'I am another foo'
promise1 = batch.saveRecord(foo1).then (foo) ->
console.log(foo)
, (error) ->
console.log(error)
promise2 = batch.saveRecord(foo2)
batch.save()
# You could also do something like:
#
# Ember.RSVP.all([promise1, promise2]).then (results) ->
# # all records saved successfully
ApplicationAdapter = DS.ActiveModelAdapter.extend
# Get an 'op' object for the record that is analagous to a normal AJAX request
opForRecord: (record, store) ->
type = record.constructor
typeKey = type.typeKey
if record.get('isNew')
method = 'post'
else if record.get('isDeleted')
method = 'delete'
else
method = 'put'
namespace = @get('namespace')
path = @pathForType(typeKey)
urlParts = [namespace, path]
urlParts.push recordId if recordId = record.get('id')
url = '/' + urlParts.join('/')
data = {}
serializer = store.serializerFor(typeKey)
serializer.serializeIntoHash(data, type, record, { includeId: method == 'post' })
op = {
method: method,
url: url
params: data
}
# Store a defer() that will be (immediately) resolved
# when an op for the record is acquired
# Also let's this adapter know that the
# next save will be done in batch mode
# TODO: Avoid storing directly on the record
acquireBatchMetaForRecord: (record, deferred) ->
record.set('batchDeferred', deferred)
batchDeferredForRecord: (record) ->
record.get('batchDeferred')
removeBatchDeferredForRecord: (record) ->
record.set('batchDeferred', null)
# Check to see if the BatchAdapter is asking for a meta object
# and return a (different) promise that will ultimately get resolved/rejected
# in BatchAdapter once the batch repsonse comes back
resolveBatchSave: (store, record) ->
if batchDeferred = @batchDeferredForRecord(record)
recordSaveResolver = Ember.RSVP.defer()
op = @opForRecord(record, store)
meta = { record: record, resolver: recordSaveResolver, op: op }
batchDeferred.resolve(meta)
@removeBatchDeferredForRecord(record)
recordSaveResolver.promise
createRecord: (store, type, record) ->
if batchPromise = @resolveBatchSave(store, record) then batchPromise else @_super.apply(@, arguments)
updateRecord: (store, type, record) ->
if batchPromise = @resolveBatchSave(store, record) then batchPromise else @_super.apply(@, arguments)
deleteRecord: (store, type, record) ->
if batchPromise = @resolveBatchSave(store, record) then batchPromise else @_super.apply(@, arguments)
# You'll need to use registerTransform here
ArrayTransform = DS.Transform.extend({
deserialize: function(serialized) {
return serialized;
},
serialize: function(deserialized) {
return deserialized;
}
});
Batch = DS.Model.extend
items: DS.attr('array', { defaultValue: -> [] })
saveRecord: (record) ->
resolver = Ember.RSVP.defer()
@get('items').pushObject
record: record
method: 'save'
resolver: resolver
resolver.promise
rejectingPromise = (error) ->
new Ember.RSVP.Promise (resolve, reject) ->
reject(error)
BatchAdapter = ApplicationAdapter.extend
find: -> rejectingPromise("You cannot find a Batch record")
updateRecord: -> rejectingPromise("You cannot update a Batch record")
deleteRecord: -> rejectingPromise("You cannot delete a Batch record")
pathForType: ->
'batch'
createRecord: (store, type, record) ->
batchAdapter = @
batch = record
items = batch.get('items')
metaPromises = []
items.forEach (item, i) ->
record = item.record
method = item.method
recordType = record.constructor
recordAdapter = store.adapterFor(recordType)
# Create the resolver that gets passed to the record's adapter via `acquireBatchMetaForRecord`
metaResolver = Ember.RSVP.defer()
metaPromises.push metaResolver.promise
recordAdapter.acquireBatchMetaForRecord(record, metaResolver)
# Call the adapter's `save` method
recordPromise = record[method].apply(record)
# If callbacks were registered for `someBatchRecord.saveRecord`, schedule them
# for when the record's `save` method is done
if itemResolver = item.resolver
recordPromise.then(itemResolver.resolve, itemResolver.reject)
# The resolver used for the batch record's `save`
batchDeferred = Ember.RSVP.defer()
batchPromise = batchDeferred.promise
# When all meta objects have been acquired
Ember.RSVP.all(metaPromises).then (recordMetas) ->
data = { ops: [], sequential: true }
recordMetas.forEach (meta) ->
data.ops.push meta.op
# Fire off the batch AJAX request
ajaxPromise = batchAdapter.ajax(batchAdapter.buildURL(type.typeKey), 'POST', { data: data })
# Resolve the promise for `batch.save`
ajaxPromise.then (payload) ->
batchDeferred.resolve(payload)
, (error) ->
batchDeferred.reject(error)
ajaxPromise.then (payload) ->
results = payload.results
# Resolve the promise for each item in the batch
results.forEach (result, i) ->
resolver = recordMetas[i].resolver
status = result.status
if status >= 200 && status < 300 || status == 304
json = result.body
resolver.resolve(json)
else
resolver.reject(batchAdapter.opError(result))
, (error) ->
recordMetas.forEach (meta) ->
meta.resolver.reject(error)
batchPromise
opError: (result) ->
if result.status == 422
jsonErrors = result.body.errors
errors = {}
Ember.keys(jsonErrors).forEach (key) ->
errors[Ember.String.camelize(key)] = jsonErrors[key]
new DS.InvalidError(errors)
else
result
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment