brainstem-js 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +3 -6
- data/Gemfile.lock +1 -1
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/lib/brainstem/js/version.rb +1 -1
- data/spec/brainstem-collection-spec.coffee +25 -0
- data/spec/{brianstem-expectation-spec.coffee → brainstem-expectation-spec.coffee} +132 -17
- data/spec/brainstem-model-spec.coffee +67 -27
- data/spec/brainstem-sync-spec.coffee +29 -6
- data/spec/brainstem-utils-spec.coffee +11 -1
- data/spec/helpers/builders.coffee +2 -2
- data/spec/helpers/models/post.coffee +1 -1
- data/spec/helpers/models/project.coffee +1 -1
- data/spec/helpers/models/task.coffee +2 -2
- data/spec/helpers/models/time-entry.coffee +1 -1
- data/spec/helpers/models/user.coffee +1 -1
- data/spec/helpers/spec-helper.coffee +21 -0
- data/spec/loaders/abstract-loader-shared-behavior.coffee +604 -0
- data/spec/loaders/abstract-loader-spec.coffee +3 -0
- data/spec/loaders/collection-loader-spec.coffee +146 -0
- data/spec/loaders/model-loader-spec.coffee +99 -0
- data/spec/storage-manager-spec.coffee +242 -56
- data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +16 -6
- data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +70 -20
- data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +13 -13
- data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +8 -3
- data/vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee +289 -0
- data/vendor/assets/javascripts/brainstem/loaders/collection-loader.coffee +68 -0
- data/vendor/assets/javascripts/brainstem/loaders/model-loader.coffee +35 -0
- data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +1 -7
- data/vendor/assets/javascripts/brainstem/storage-manager.coffee +79 -196
- data/vendor/assets/javascripts/brainstem/utils.coffee +18 -4
- metadata +17 -6
@@ -18,22 +18,32 @@ class window.Brainstem.Collection extends Backbone.Collection
|
|
18
18
|
else
|
19
19
|
Brainstem.Utils.warn "Unable to update collection with invalid model", model
|
20
20
|
|
21
|
-
loadNextPage: (options)
|
21
|
+
loadNextPage: (options) ->
|
22
22
|
oldLength = @length
|
23
|
+
pageSize = 0
|
24
|
+
paginationOptions = {}
|
25
|
+
|
26
|
+
if @lastFetchOptions.perPage
|
27
|
+
paginationOptions.page = @lastFetchOptions.page + 1
|
28
|
+
pageSize = @lastFetchOptions.perPage
|
29
|
+
else
|
30
|
+
paginationOptions.offset = @lastFetchOptions.offset + @lastFetchOptions.limit
|
31
|
+
pageSize = @lastFetchOptions.limit
|
32
|
+
|
23
33
|
success = (collection) =>
|
24
|
-
options.success(collection, collection.length == oldLength +
|
25
|
-
base.data.loadCollection @lastFetchOptions.name, _.extend({}, @lastFetchOptions, options,
|
34
|
+
options.success(collection, collection.length == oldLength + pageSize) if options.success?
|
35
|
+
base.data.loadCollection @lastFetchOptions.name, _.extend({}, @lastFetchOptions, options, paginationOptions, collection: this, success: success)
|
26
36
|
|
27
|
-
reload: (options)
|
37
|
+
reload: (options) ->
|
28
38
|
base.data.reset()
|
29
39
|
@reset [], silent: true
|
30
40
|
@setLoaded false
|
31
41
|
base.data.loadCollection @lastFetchOptions.name, _.extend({}, @lastFetchOptions, options, page: 1, collection: this)
|
32
42
|
|
33
|
-
getWithAssocation: (id)
|
43
|
+
getWithAssocation: (id) ->
|
34
44
|
@get(id)
|
35
45
|
|
36
|
-
toServerJSON: (method)
|
46
|
+
toServerJSON: (method) ->
|
37
47
|
@toJSON()
|
38
48
|
|
39
49
|
@getComparatorWithIdFailover: (order) ->
|
@@ -1,13 +1,13 @@
|
|
1
1
|
window.Brainstem ?= {}
|
2
2
|
|
3
3
|
class window.Brainstem.Expectation
|
4
|
-
constructor: (
|
5
|
-
@
|
4
|
+
constructor: (name, options, manager) ->
|
5
|
+
@name = name
|
6
6
|
@manager = manager
|
7
|
-
@manager.
|
7
|
+
@manager._setDefaultPageSettings options
|
8
8
|
@options = options
|
9
|
-
@results = []
|
10
9
|
@matches = []
|
10
|
+
@recursive = false
|
11
11
|
@triggerError = options.triggerError
|
12
12
|
@immediate = options.immediate
|
13
13
|
delete options.immediate
|
@@ -16,50 +16,100 @@ class window.Brainstem.Expectation
|
|
16
16
|
@requestQueue = []
|
17
17
|
@options.response(@) if @options.response?
|
18
18
|
|
19
|
-
remove:
|
19
|
+
remove: ->
|
20
20
|
@disabled = true
|
21
21
|
|
22
|
-
recordRequest: (
|
22
|
+
recordRequest: (loader) ->
|
23
23
|
if @immediate
|
24
|
-
@handleRequest
|
24
|
+
@handleRequest(loader)
|
25
25
|
else
|
26
|
-
@requestQueue.push
|
26
|
+
@requestQueue.push(loader)
|
27
27
|
|
28
|
-
respond:
|
28
|
+
respond: ->
|
29
29
|
for request in @requestQueue
|
30
30
|
@handleRequest request
|
31
31
|
@requestQueue = []
|
32
32
|
|
33
|
-
handleRequest: (
|
34
|
-
@matches.push
|
33
|
+
handleRequest: (loader) ->
|
34
|
+
@matches.push loader.originalOptions
|
35
|
+
|
36
|
+
unless @recursive
|
37
|
+
# we don't need to fetch additional things from the server in an expectation.
|
38
|
+
loader.loadOptions.include = []
|
35
39
|
|
36
40
|
if @triggerError?
|
37
|
-
return @manager.errorInterceptor(
|
41
|
+
return @manager.errorInterceptor(loader.originalOptions.error, loader.externalObject, loader.originalOptions, @triggerError)
|
42
|
+
|
43
|
+
@_handleAssociations(loader)
|
44
|
+
|
45
|
+
if loader instanceof Brainstem.CollectionLoader
|
46
|
+
returnedData = @_handleCollectionResults(loader)
|
47
|
+
else
|
48
|
+
returnedData = @_handleModelResults(loader)
|
49
|
+
|
50
|
+
loader._onLoadSuccess(returnedData)
|
51
|
+
|
52
|
+
loaderOptionsMatch: (loader) ->
|
53
|
+
return false if @disabled
|
54
|
+
return false if @name != loader._getExpectationName()
|
55
|
+
|
56
|
+
@manager._checkPageSettings(loader.originalOptions)
|
57
|
+
|
58
|
+
_.all ['include', 'only', 'order', 'filters', 'perPage', 'page', 'limit', 'offset', 'search'], (optionType) =>
|
59
|
+
return true if @options[optionType] == '*'
|
60
|
+
|
61
|
+
option = _.compact(_.flatten([loader.originalOptions[optionType]]))
|
62
|
+
expectedOption = _.compact(_.flatten([@options[optionType]]))
|
38
63
|
|
64
|
+
if optionType == 'include'
|
65
|
+
option = Brainstem.Utils.wrapObjects(option)
|
66
|
+
expectedOption = Brainstem.Utils.wrapObjects(expectedOption)
|
67
|
+
|
68
|
+
Brainstem.Utils.matches(option, expectedOption)
|
69
|
+
|
70
|
+
_handleAssociations: (_loader) ->
|
39
71
|
for key, values of @associated
|
40
72
|
values = [values] unless values instanceof Array
|
41
73
|
for value in values
|
42
74
|
@manager.storage(value.brainstemKey).update [value]
|
43
75
|
|
76
|
+
_handleCollectionResults: (loader) ->
|
77
|
+
return if not @results
|
78
|
+
|
44
79
|
for result in @results
|
45
80
|
if result instanceof Brainstem.Model
|
46
81
|
@manager.storage(result.brainstemKey).update [result]
|
47
82
|
|
48
|
-
returnedModels = _
|
83
|
+
returnedModels = _.map @results, (result) =>
|
49
84
|
if result instanceof Brainstem.Model
|
50
85
|
@manager.storage(result.brainstemKey).get(result.id)
|
51
86
|
else
|
52
87
|
@manager.storage(result.key).get(result.id)
|
53
88
|
|
54
|
-
|
89
|
+
returnedModels
|
90
|
+
|
91
|
+
_handleModelResults: (loader) ->
|
92
|
+
return if !@result
|
55
93
|
|
56
|
-
|
57
|
-
@
|
58
|
-
|
59
|
-
|
60
|
-
@options[optionType] == "*" || Brainstem.Utils.matches(_.compact(_.flatten([options[optionType]])), _.compact(_.flatten([@options[optionType]])))
|
94
|
+
# Put main (loader) model in storage manager.
|
95
|
+
if @result instanceof Brainstem.Model
|
96
|
+
key = @result.brainstemKey
|
97
|
+
attributes = @result.attributes
|
61
98
|
else
|
62
|
-
|
99
|
+
key = @result.key
|
100
|
+
attributes = _.omit @result, 'key'
|
101
|
+
|
102
|
+
if !key
|
103
|
+
throw 'Brainstem key is required on the result (brainstemKey on model or key in JSON)'
|
104
|
+
|
105
|
+
existingModel = @manager.storage(key).get(attributes.id)
|
106
|
+
|
107
|
+
unless existingModel
|
108
|
+
existingModel = loader.getModel()
|
109
|
+
@manager.storage(key).add(existingModel)
|
110
|
+
|
111
|
+
existingModel.set(attributes)
|
112
|
+
existingModel
|
63
113
|
|
64
114
|
lastMatch: ->
|
65
115
|
@matches[@matches.length - 1]
|
@@ -2,10 +2,6 @@
|
|
2
2
|
|
3
3
|
# Extend Backbone.Model to include associations.
|
4
4
|
class window.Brainstem.Model extends Backbone.Model
|
5
|
-
constructor: ->
|
6
|
-
super
|
7
|
-
@setLoaded false
|
8
|
-
|
9
5
|
# Parse ISO8601 attribute strings into date objects
|
10
6
|
@parse: (modelObject) ->
|
11
7
|
for k,v of modelObject
|
@@ -15,7 +11,7 @@ class window.Brainstem.Model extends Backbone.Model
|
|
15
11
|
return modelObject
|
16
12
|
|
17
13
|
# Handle create and update responses with JSON root keys
|
18
|
-
parse: (resp, xhr)
|
14
|
+
parse: (resp, xhr) ->
|
19
15
|
@updateStorageManager(resp)
|
20
16
|
modelObject = @_parseResultsResponse(resp)
|
21
17
|
super(@constructor.parse(modelObject), xhr)
|
@@ -38,7 +34,11 @@ class window.Brainstem.Model extends Backbone.Model
|
|
38
34
|
if collectionModel
|
39
35
|
collectionModel.set(attributes)
|
40
36
|
else
|
41
|
-
|
37
|
+
if @brainstemKey == underscoredModelName && (@isNew() || @id == attributes.id)
|
38
|
+
@set(attributes)
|
39
|
+
collection.add(this)
|
40
|
+
else
|
41
|
+
collection.add(attributes)
|
42
42
|
|
43
43
|
_parseResultsResponse: (resp) ->
|
44
44
|
return resp unless resp['results']
|
@@ -75,8 +75,10 @@ class window.Brainstem.Model extends Backbone.Model
|
|
75
75
|
# provided, all associations are assumed.
|
76
76
|
# model.associationsAreLoaded(["project", "task"]) # => true|false
|
77
77
|
# model.associationsAreLoaded() # => true|false
|
78
|
-
associationsAreLoaded: (associations)
|
78
|
+
associationsAreLoaded: (associations) ->
|
79
79
|
associations ||= _.keys(@constructor.associations)
|
80
|
+
associations = _.select associations, (association) => @constructor.associationDetails(association)
|
81
|
+
|
80
82
|
_.all associations, (association) =>
|
81
83
|
details = @constructor.associationDetails(association)
|
82
84
|
if details.type == "BelongsTo"
|
@@ -85,7 +87,7 @@ class window.Brainstem.Model extends Backbone.Model
|
|
85
87
|
@attributes.hasOwnProperty(details.key) && _.all(@attributes[details.key], (id) -> base.data.storage(details.collectionName).get(id))
|
86
88
|
|
87
89
|
# Override Model#get to access associations as well as fields.
|
88
|
-
get: (field, options = {})
|
90
|
+
get: (field, options = {}) ->
|
89
91
|
if details = @constructor.associationDetails(field)
|
90
92
|
if details.type == "BelongsTo"
|
91
93
|
id = @get(details.key) # project_id
|
@@ -111,7 +113,7 @@ class window.Brainstem.Model extends Backbone.Model
|
|
111
113
|
else
|
112
114
|
super(field)
|
113
115
|
|
114
|
-
className:
|
116
|
+
className: ->
|
115
117
|
@paramRoot
|
116
118
|
|
117
119
|
defaultJSONBlacklist: ->
|
@@ -123,7 +125,7 @@ class window.Brainstem.Model extends Backbone.Model
|
|
123
125
|
updateJSONBlacklist: ->
|
124
126
|
[]
|
125
127
|
|
126
|
-
toServerJSON: (method, options)
|
128
|
+
toServerJSON: (method, options) ->
|
127
129
|
json = @toJSON(options)
|
128
130
|
blacklist = @defaultJSONBlacklist()
|
129
131
|
|
@@ -136,6 +138,4 @@ class window.Brainstem.Model extends Backbone.Model
|
|
136
138
|
for blacklistKey in blacklist
|
137
139
|
delete json[blacklistKey]
|
138
140
|
|
139
|
-
json
|
140
|
-
|
141
|
-
_.extend(Brainstem.Model.prototype, Brainstem.LoadingMixin);
|
141
|
+
json
|
@@ -36,7 +36,7 @@ Backbone.sync = (method, model, options) ->
|
|
36
36
|
else
|
37
37
|
data = json
|
38
38
|
|
39
|
-
data.include = Brainstem.Utils.extractArray("include", options).join("
|
39
|
+
data.include = Brainstem.Utils.extractArray("include", options).join(",")
|
40
40
|
data.filters = Brainstem.Utils.extractArray("filters", options).join(",")
|
41
41
|
params.data = JSON.stringify(data)
|
42
42
|
|
@@ -57,6 +57,10 @@ Backbone.sync = (method, model, options) ->
|
|
57
57
|
if beforeSend
|
58
58
|
beforeSend.apply this, arguments
|
59
59
|
|
60
|
+
# Clear out default data for DELETE requests, fixes a firefox issue where this exception is thrown: JavaScript component does not have a method named: “available”
|
61
|
+
if params.type == 'DELETE'
|
62
|
+
params.data = null
|
63
|
+
|
60
64
|
# Don't process data on a non-GET request.
|
61
65
|
if params.type != 'GET' && !options.emulateJSON
|
62
66
|
params.processData = false
|
@@ -67,8 +71,9 @@ Backbone.sync = (method, model, options) ->
|
|
67
71
|
if params.type == 'PATCH' && window.ActiveXObject && !(window.external && window.external.msActiveXFilteringEnabled)
|
68
72
|
params.xhr = -> new ActiveXObject("Microsoft.XMLHTTP")
|
69
73
|
|
70
|
-
|
71
|
-
|
74
|
+
if base?.data?.errorInterceptor?
|
75
|
+
errorHandler = options.error
|
76
|
+
options.error = (jqXHR, textStatus, errorThrown) -> base.data.errorInterceptor(errorHandler, model, options, jqXHR, params)
|
72
77
|
|
73
78
|
# Make the request, allowing the user to override any Ajax options.
|
74
79
|
xhr = options.xhr = Backbone.ajax(_.extend(params, options))
|
@@ -0,0 +1,289 @@
|
|
1
|
+
window.Brainstem ?= {}
|
2
|
+
|
3
|
+
class Brainstem.AbstractLoader
|
4
|
+
internalObject: null
|
5
|
+
externalObject: null
|
6
|
+
|
7
|
+
constructor: (options = {}) ->
|
8
|
+
@storageManager = options.storageManager
|
9
|
+
|
10
|
+
@_deferred = $.Deferred()
|
11
|
+
@_deferred.promise(this)
|
12
|
+
|
13
|
+
if options.loadOptions
|
14
|
+
@setup(options.loadOptions)
|
15
|
+
|
16
|
+
###*
|
17
|
+
* Setup the loader with a list of Brainstem specific loadOptions
|
18
|
+
* @param {object} loadOptions Brainstem specific loadOptions (filters, include, only, etc)
|
19
|
+
* @return {object} externalObject that was created.
|
20
|
+
###
|
21
|
+
setup: (loadOptions) ->
|
22
|
+
@_parseLoadOptions(loadOptions)
|
23
|
+
@_createObjects()
|
24
|
+
|
25
|
+
@externalObject
|
26
|
+
|
27
|
+
###*
|
28
|
+
* Parse supplied loadOptions, add defaults, transform them into appropriate structures, and pull out important pieces.
|
29
|
+
* @param {object} loadOptions
|
30
|
+
* @return {object} transformed loadOptions
|
31
|
+
###
|
32
|
+
_parseLoadOptions: (loadOptions = {}) ->
|
33
|
+
@originalOptions = _.clone(loadOptions)
|
34
|
+
@loadOptions = _.clone(loadOptions)
|
35
|
+
@loadOptions.include = Brainstem.Utils.wrapObjects(Brainstem.Utils.extractArray "include", @loadOptions)
|
36
|
+
@loadOptions.only = if @loadOptions.only then _.map((Brainstem.Utils.extractArray "only", @loadOptions), (id) -> String(id)) else null
|
37
|
+
@loadOptions.filters ?= {}
|
38
|
+
@loadOptions.thisLayerInclude = _.map @loadOptions.include, (i) -> _.keys(i)[0] # pull off the top layer of includes
|
39
|
+
|
40
|
+
# Determine whether or not we should look at the cache
|
41
|
+
@loadOptions.cache ?= true
|
42
|
+
@loadOptions.cache = false if @loadOptions.search
|
43
|
+
|
44
|
+
# Build cache key
|
45
|
+
filterKeys = _.map(@loadOptions.filters, (v, k) -> "#{k}:#{v}").join(',')
|
46
|
+
@loadOptions.cacheKey = [@loadOptions.order || "updated_at:desc", filterKeys, @loadOptions.page, @loadOptions.perPage, @loadOptions.limit, @loadOptions.offset].join('|')
|
47
|
+
|
48
|
+
@cachedCollection = @storageManager.storage @_getCollectionName()
|
49
|
+
|
50
|
+
@loadOptions
|
51
|
+
|
52
|
+
###*
|
53
|
+
* Sets up both the `internalObject` and `externalObject`.
|
54
|
+
* In the case of models the `internalObject` and `externalObject` are the same.
|
55
|
+
* In the case of collections the `internalObject` is a proxy object that updates the `externalObject` when all loading is completed.
|
56
|
+
* @return {[type]} [description]
|
57
|
+
###
|
58
|
+
_createObjects: ->
|
59
|
+
throw "Implement in your subclass"
|
60
|
+
|
61
|
+
###*
|
62
|
+
* Loads the model from memory or the server.
|
63
|
+
* @return {object} the loader's `externalObject`
|
64
|
+
###
|
65
|
+
load: ->
|
66
|
+
if not @loadOptions
|
67
|
+
throw "You must call #setup first or pass loadOptions into the constructor"
|
68
|
+
|
69
|
+
# Check the cache to see if we have everything that we need.
|
70
|
+
if @loadOptions.cache && data = @_checkCacheForData()
|
71
|
+
data
|
72
|
+
else
|
73
|
+
@_loadFromServer()
|
74
|
+
|
75
|
+
###*
|
76
|
+
* Checks to see if the current requested data is available in the caching layer.
|
77
|
+
* If it is available then update the externalObject with that data (via `_onLoadSuccess`).
|
78
|
+
* @return {[boolean|object]} returns false if not found otherwise returns the externalObject.
|
79
|
+
###
|
80
|
+
_checkCacheForData: ->
|
81
|
+
if @loadOptions.only?
|
82
|
+
@alreadyLoadedIds = _.select @loadOptions.only, (id) => @cachedCollection.get(id)?.associationsAreLoaded(@loadOptions.thisLayerInclude)
|
83
|
+
if @alreadyLoadedIds.length == @loadOptions.only.length
|
84
|
+
# We've already seen every id that is being asked for and have all the associated data.
|
85
|
+
@_onLoadSuccess(_.map @loadOptions.only, (id) => @cachedCollection.get(id))
|
86
|
+
return @externalObject
|
87
|
+
else
|
88
|
+
# Check if we have a cache for this request and if so make sure that all of the requested includes for this layer are loaded on those models.
|
89
|
+
if @storageManager.getCollectionDetails(@_getCollectionName()).cache[@loadOptions.cacheKey]
|
90
|
+
subset = _(@storageManager.getCollectionDetails(@_getCollectionName()).cache[@loadOptions.cacheKey]).map (result) => @storageManager.storage(result.key).get(result.id)
|
91
|
+
if (_.all(subset, (model) => model.associationsAreLoaded(@loadOptions.thisLayerInclude)))
|
92
|
+
@_onLoadSuccess(subset)
|
93
|
+
return @externalObject
|
94
|
+
|
95
|
+
return false
|
96
|
+
|
97
|
+
###*
|
98
|
+
* Makes a GET request to the server via Backbone.sync with the built syncOptions.
|
99
|
+
* @return {object} externalObject that will be updated when everything is complete.
|
100
|
+
###
|
101
|
+
_loadFromServer: ->
|
102
|
+
jqXhr = Backbone.sync.call @internalObject, 'read', @internalObject, @_buildSyncOptions()
|
103
|
+
|
104
|
+
if @loadOptions.returnValues
|
105
|
+
@loadOptions.returnValues.jqXhr = jqXhr
|
106
|
+
|
107
|
+
@externalObject
|
108
|
+
|
109
|
+
###*
|
110
|
+
* Called when the Backbone.sync successfully responds from the server.
|
111
|
+
* @param {object} resp JSON response from the server.
|
112
|
+
* @param {string} _status
|
113
|
+
* @param {object} _xhr jQuery XHR object
|
114
|
+
* @return {undefined}
|
115
|
+
###
|
116
|
+
_onServerLoadSuccess: (resp, _status, _xhr) =>
|
117
|
+
data = @_updateStorageManagerFromResponse(resp)
|
118
|
+
@_onLoadSuccess(data)
|
119
|
+
|
120
|
+
###*
|
121
|
+
* Called when the server responds with data and needs to be persisted to the storageManager.
|
122
|
+
* @param {object} resp JSON data from the server
|
123
|
+
* @return {[array|object]} array of models or model that was parsed.
|
124
|
+
###
|
125
|
+
_updateStorageManagerFromResponse: (resp) ->
|
126
|
+
throw "Implement in your subclass"
|
127
|
+
|
128
|
+
###*
|
129
|
+
* Updates the internalObject with the data in the storageManager and either loads more data or resolves this load.
|
130
|
+
* Called after sync + storage manager upadting.
|
131
|
+
* @param {array|object} data array of models or model from _updateStorageManagerFromResponse
|
132
|
+
* @return {undefined}
|
133
|
+
###
|
134
|
+
_onLoadSuccess: (data) ->
|
135
|
+
@_updateObjects(@internalObject, data, true)
|
136
|
+
@_calculateAdditionalIncludes()
|
137
|
+
|
138
|
+
if @additionalIncludes.length
|
139
|
+
@_loadAdditionalIncludes()
|
140
|
+
else
|
141
|
+
@_onLoadingCompleted()
|
142
|
+
|
143
|
+
###*
|
144
|
+
* Called after the server responds with the first layer of includes to determine if any more loads are needed.
|
145
|
+
* It will only make additional loads if there were IDs returned during this load for a given association.
|
146
|
+
* @return {undefined}
|
147
|
+
###
|
148
|
+
_calculateAdditionalIncludes: ->
|
149
|
+
@additionalIncludes = []
|
150
|
+
|
151
|
+
for hash in @loadOptions.include
|
152
|
+
associationName = _.keys(hash)[0]
|
153
|
+
associationIds = @_getIdsForAssociation(associationName)
|
154
|
+
associationInclude = hash[associationName]
|
155
|
+
|
156
|
+
if associationIds.length && associationInclude.length
|
157
|
+
@additionalIncludes.push
|
158
|
+
name: associationName
|
159
|
+
ids: associationIds
|
160
|
+
include: associationInclude
|
161
|
+
|
162
|
+
###*
|
163
|
+
* Loads the next layer of includes from the server.
|
164
|
+
* When all loads are complete, it will call `_onLoadingCompleted` which will resolve this layer.
|
165
|
+
* @return {undefined}
|
166
|
+
###
|
167
|
+
_loadAdditionalIncludes: ->
|
168
|
+
promises = []
|
169
|
+
|
170
|
+
for association in @additionalIncludes
|
171
|
+
collectionName = @_getModel().associationDetails(association.name).collectionName
|
172
|
+
|
173
|
+
loadOptions =
|
174
|
+
only: association.ids
|
175
|
+
include: association.include
|
176
|
+
error: @loadOptions.error
|
177
|
+
|
178
|
+
promises.push(@storageManager.loadObject(collectionName, loadOptions))
|
179
|
+
|
180
|
+
$.when.apply($, promises).done(@_onLoadingCompleted)
|
181
|
+
|
182
|
+
###*
|
183
|
+
* Called when all loading (including nested loads) are complete.
|
184
|
+
* Updates the `externalObject` with the data that was gathered and resolves the promise.
|
185
|
+
* @return {undefined}
|
186
|
+
###
|
187
|
+
_onLoadingCompleted: =>
|
188
|
+
@_updateObjects(@externalObject, @internalObject)
|
189
|
+
@_deferred.resolve(@externalObject)
|
190
|
+
|
191
|
+
###*
|
192
|
+
* Updates the object with the supplied data. Will be called:
|
193
|
+
* + after the server responds, `object` will be `internalObject` and data will be the result of `_updateStorageManagerFromResponse`
|
194
|
+
* + after all loading is complete, `object` will be the `externalObject` and data will be the `internalObject`
|
195
|
+
* @param {object} object object that will receive the data
|
196
|
+
* @param {object} data data that needs set on the object
|
197
|
+
* @param {boolean} silent whether or not to trigger loaded at the end of the update
|
198
|
+
* @return {undefined}
|
199
|
+
###
|
200
|
+
_updateObjects: (object, data, silent = false) ->
|
201
|
+
throw "Implement in your subclass"
|
202
|
+
|
203
|
+
###*
|
204
|
+
* Generates the Brainstem specific options that are passed to Backbone.sync.
|
205
|
+
* @return {object} options that are passed to Backbone.sync
|
206
|
+
###
|
207
|
+
_buildSyncOptions: ->
|
208
|
+
syncOptions =
|
209
|
+
data: {}
|
210
|
+
parse: true
|
211
|
+
error: @loadOptions.error
|
212
|
+
success: @_onServerLoadSuccess
|
213
|
+
|
214
|
+
syncOptions.data.include = @loadOptions.thisLayerInclude.join(",") if @loadOptions.thisLayerInclude.length
|
215
|
+
|
216
|
+
if @loadOptions.only && @_shouldUseOnly()
|
217
|
+
syncOptions.data.only = _.difference(@loadOptions.only, @alreadyLoadedIds).join(",")
|
218
|
+
|
219
|
+
syncOptions.data.order = @loadOptions.order if @loadOptions.order?
|
220
|
+
_.extend(syncOptions.data, _(@loadOptions.filters).omit('include', 'only', 'order', 'per_page', 'page', 'limit', 'offset', 'search')) if _(@loadOptions.filters).keys().length
|
221
|
+
|
222
|
+
unless @loadOptions.only?
|
223
|
+
if @loadOptions.limit? && @loadOptions.offset?
|
224
|
+
syncOptions.data.limit = @loadOptions.limit
|
225
|
+
syncOptions.data.offset = @loadOptions.offset
|
226
|
+
else
|
227
|
+
syncOptions.data.per_page = @loadOptions.perPage
|
228
|
+
syncOptions.data.page = @loadOptions.page
|
229
|
+
|
230
|
+
syncOptions.data.search = @loadOptions.search if @loadOptions.search
|
231
|
+
syncOptions
|
232
|
+
|
233
|
+
###*
|
234
|
+
* Decides whether or not the `only` filter should be applied in the syncOptions.
|
235
|
+
* Models will not use the `only` filter as they use show routes.
|
236
|
+
* @return {boolean} whether or not to use the `only` filter
|
237
|
+
###
|
238
|
+
_shouldUseOnly: ->
|
239
|
+
@internalObject instanceof Backbone.Collection
|
240
|
+
|
241
|
+
###*
|
242
|
+
* Returns the name of the collection that this loader maps to and will update in the storageManager.
|
243
|
+
* @return {string} name of the collection
|
244
|
+
###
|
245
|
+
_getCollectionName: ->
|
246
|
+
throw "Implement in your subclass"
|
247
|
+
|
248
|
+
###*
|
249
|
+
* Returns the name that expectations will be stubbed with (story or stories etc)
|
250
|
+
* @return {string} name of the stub
|
251
|
+
###
|
252
|
+
_getExpectationName: ->
|
253
|
+
throw "Implement in your subclass"
|
254
|
+
|
255
|
+
###*
|
256
|
+
* This needs to return a constructor for the model that associations will be compared with.
|
257
|
+
* This typically will be the current collection's model/current model constructor.
|
258
|
+
* @return {Brainstem.Model}
|
259
|
+
###
|
260
|
+
_getModel: ->
|
261
|
+
throw "Implement in your subclass"
|
262
|
+
|
263
|
+
###*
|
264
|
+
* This needs to return an array of models that correspond to the supplied association.
|
265
|
+
* @return {array} models that are associated with this association
|
266
|
+
###
|
267
|
+
_getModelsForAssociation: (association) ->
|
268
|
+
throw "Implement in your subclass"
|
269
|
+
|
270
|
+
###*
|
271
|
+
* Returns an array of IDs that need to be loaded for this association.
|
272
|
+
* @param {string} association name of the association
|
273
|
+
* @return {array} array of IDs to fetch for this association.
|
274
|
+
###
|
275
|
+
_getIdsForAssociation: (association) ->
|
276
|
+
models = @_getModelsForAssociation(association)
|
277
|
+
_(models).chain().flatten().pluck("id").compact().uniq().sort().value()
|
278
|
+
|
279
|
+
###*
|
280
|
+
* Parses the result of model.get(associationName) to either return a collection's models
|
281
|
+
* or the model itself.
|
282
|
+
* @param {object|Backbone.Collection} obj result of calling `.get` on a model with an association name.
|
283
|
+
* @return {object|array} either a model object or an array of models from a collection.
|
284
|
+
###
|
285
|
+
_modelsOrObj: (obj) ->
|
286
|
+
if obj instanceof Backbone.Collection
|
287
|
+
obj.models
|
288
|
+
else
|
289
|
+
obj || [] # TODO: revisit this.. we shouldn't be getting to this stage.
|