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.
- 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.
|