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.
Files changed (33) hide show
  1. data/.travis.yml +3 -6
  2. data/Gemfile.lock +1 -1
  3. data/README.md +3 -1
  4. data/Rakefile +2 -0
  5. data/lib/brainstem/js/version.rb +1 -1
  6. data/spec/brainstem-collection-spec.coffee +25 -0
  7. data/spec/{brianstem-expectation-spec.coffee → brainstem-expectation-spec.coffee} +132 -17
  8. data/spec/brainstem-model-spec.coffee +67 -27
  9. data/spec/brainstem-sync-spec.coffee +29 -6
  10. data/spec/brainstem-utils-spec.coffee +11 -1
  11. data/spec/helpers/builders.coffee +2 -2
  12. data/spec/helpers/models/post.coffee +1 -1
  13. data/spec/helpers/models/project.coffee +1 -1
  14. data/spec/helpers/models/task.coffee +2 -2
  15. data/spec/helpers/models/time-entry.coffee +1 -1
  16. data/spec/helpers/models/user.coffee +1 -1
  17. data/spec/helpers/spec-helper.coffee +21 -0
  18. data/spec/loaders/abstract-loader-shared-behavior.coffee +604 -0
  19. data/spec/loaders/abstract-loader-spec.coffee +3 -0
  20. data/spec/loaders/collection-loader-spec.coffee +146 -0
  21. data/spec/loaders/model-loader-spec.coffee +99 -0
  22. data/spec/storage-manager-spec.coffee +242 -56
  23. data/vendor/assets/javascripts/brainstem/brainstem-collection.coffee +16 -6
  24. data/vendor/assets/javascripts/brainstem/brainstem-expectation.coffee +70 -20
  25. data/vendor/assets/javascripts/brainstem/brainstem-model.coffee +13 -13
  26. data/vendor/assets/javascripts/brainstem/brainstem-sync.coffee +8 -3
  27. data/vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee +289 -0
  28. data/vendor/assets/javascripts/brainstem/loaders/collection-loader.coffee +68 -0
  29. data/vendor/assets/javascripts/brainstem/loaders/model-loader.coffee +35 -0
  30. data/vendor/assets/javascripts/brainstem/loading-mixin.coffee +1 -7
  31. data/vendor/assets/javascripts/brainstem/storage-manager.coffee +79 -196
  32. data/vendor/assets/javascripts/brainstem/utils.coffee +18 -4
  33. 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 + @lastFetchOptions.perPage) if options.success?
25
- base.data.loadCollection @lastFetchOptions.name, _.extend({}, @lastFetchOptions, options, page: @lastFetchOptions.page + 1, collection: this, success: success)
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: (collectionName, options, manager) ->
5
- @collectionName = collectionName
4
+ constructor: (name, options, manager) ->
5
+ @name = name
6
6
  @manager = manager
7
- @manager._checkPageSettings options
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: (collection, callOptions) =>
22
+ recordRequest: (loader) ->
23
23
  if @immediate
24
- @handleRequest collection: collection, callOptions: callOptions
24
+ @handleRequest(loader)
25
25
  else
26
- @requestQueue.push collection: collection, callOptions: callOptions
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: (options) =>
34
- @matches.push options.callOptions
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(options.callOptions.error, options.collection, options.callOptions, @triggerError)
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 = _(@results).map (result) =>
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
- @manager._success(options.callOptions, options.collection, returnedModels)
89
+ returnedModels
90
+
91
+ _handleModelResults: (loader) ->
92
+ return if !@result
55
93
 
56
- optionsMatch: (name, options) =>
57
- @manager._checkPageSettings options
58
- if !@disabled && @collectionName == name
59
- _(['include', 'only', 'order', 'filters', 'perPage', 'page', 'search']).all (optionType) =>
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
- false
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
- collection.add(attributes)
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
- errorHandler = options.error
71
- options.error = (jqXHR, textStatus, errorThrown) -> base?.data?.errorInterceptor?(errorHandler, model, options, jqXHR, params)
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.