brainstem-js 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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.