Last active
February 20, 2017 11:07
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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