vendor/assets/javascripts/brainstem/storage-manager.coffee in brainstem-js-0.2.1 vs vendor/assets/javascripts/brainstem/storage-manager.coffee in brainstem-js-0.3.0

- old
+ new

@@ -20,256 +20,139 @@ storage: new collectionClass() cache: {} # Access the cache for a particular collection. # manager.storage("time_entries").get(12).get("title") - storage: (name) => + storage: (name) -> @getCollectionDetails(name).storage - dataUsage: => + dataUsage: -> sum = 0 for dataType in @collectionNames() sum += @storage(dataType).length sum - reset: => + reset: -> for name, attributes of @collections attributes.storage.reset [] attributes.cache = {} # Access details of a collection. An error will be thrown if the collection cannot be found. - getCollectionDetails: (name) => + getCollectionDetails: (name) -> @collections[name] || @collectionError(name) - collectionNames: => + collectionNames: -> _.keys(@collections) - collectionExists: (name) => + collectionExists: (name) -> !!@collections[name] - setErrorInterceptor: (interceptor) => - @errorInterceptor = interceptor || (handler, modelOrCollection, options, jqXHR, requestParams) -> handler?(modelOrCollection, jqXHR) + setErrorInterceptor: (interceptor) -> + @errorInterceptor = interceptor || (handler, modelOrCollection, options, jqXHR, requestParams) -> handler?(jqXHR) - # Request a model to be loaded, optionally ensuring that associations be included as well. A collection is returned immediately and is reset - # when the load, and any dependent loads, are complete. - # model = manager.loadModel "time_entry" - # model = manager.loadModel "time_entry", fields: ["title", "notes"] - # model = manager.loadModel "time_entry", include: ["project", "task"] - loadModel: (name, id, options) => - options = _.clone(options || {}) - oldSuccess = options.success - collectionName = name.pluralize() - model = new (@getCollectionDetails(collectionName).modelKlass)() - @loadCollection collectionName, _.extend options, - only: id - success: (collection) -> - model.setLoaded true, trigger: false - model.set collection.get(id).attributes - model.setLoaded true - oldSuccess(model) if oldSuccess - model + # Request a model to be loaded, optionally ensuring that associations be included as well. A loader (which is a jQuery promise) is returned immediately and is resolved + # with the model from the StorageManager when the load, and any dependent loads, are complete. + # loader = manager.loadModel "time_entry", 2 + # loader = manager.loadModel "time_entry", 2, fields: ["title", "notes"] + # loader = manager.loadModel "time_entry", 2, include: ["project", "task"] + # manager.loadModel("time_entry", 2, include: ["project", "task"]).done (model) -> console.log model + loadModel: (name, id, options = {}) -> + return if not id + loader = @loadObject(name, $.extend({}, options, only: id), isCollection: false) + loader + # Request a set of data to be loaded, optionally ensuring that associations be included as well. A collection is returned immediately and is reset # when the load, and any dependent loads, are complete. # collection = manager.loadCollection "time_entries" # collection = manager.loadCollection "time_entries", only: [2, 6] # collection = manager.loadCollection "time_entries", fields: ["title", "notes"] # collection = manager.loadCollection "time_entries", include: ["project", "task"] # collection = manager.loadCollection "time_entries", include: ["project:title,description", "task:due_date"] # collection = manager.loadCollection "tasks", include: ["assets", { "assignees": "account" }, { "sub_tasks": ["assignees", "assets"] }] # collection = manager.loadCollection "time_entries", filters: ["project_id:6", "editable:true"], order: "updated_at:desc", page: 1, perPage: 20 - loadCollection: (name, options) => - options = $.extend({}, options, name: name) - @_checkPageSettings options - include = @_wrapObjects(Brainstem.Utils.extractArray "include", options) - if options.search - options.cache = false + loadCollection: (name, options = {}) -> + loader = @loadObject(name, options) + loader.externalObject - collection = options.collection || @createNewCollection name, [] - collection.setLoaded false - collection.reset([], silent: false) if options.reset - collection.lastFetchOptions = _.pick($.extend(true, {}, options), 'name', 'filters', 'include', 'page', 'perPage', 'order', 'search') - - if @expectations? - @handleExpectations name, collection, options - else - @_loadCollectionWithFirstLayer($.extend({}, options, include: include, success: ((firstLayerCollection) => - expectedAdditionalLoads = @_countRequiredServerRequests(include) - 1 - if expectedAdditionalLoads > 0 - timesCalled = 0 - @_handleNextLayer firstLayerCollection, include, => - timesCalled += 1 - if timesCalled == expectedAdditionalLoads - @_success(options, collection, firstLayerCollection) - else - @_success(options, collection, firstLayerCollection) - ))) - - collection - - _handleNextLayer: (collection, include, callback) => - # Collection is a fully populated collection of tasks whose first layer of associations are loaded. - # include is a hierarchical list of associations on those tasks: - # [{ 'time_entries': ['project': [], 'task': [{ 'assignees': []}]] }, { 'project': [] }] - - _(include).each (hash) => # { 'time_entries': ['project': [], 'task': [{ 'assignees': []}]] } - association = _.keys(hash)[0] # time_entries - nextLevelInclude = hash[association] # ['project': [], 'task': [{ 'assignees': []}]] - if nextLevelInclude.length - association_ids = _(collection.models).chain(). - map((m) -> if (a = m.get(association)) instanceof Backbone.Collection then a.models else a). - flatten().uniq().compact().pluck("id").sort().value() - newCollectionName = collection.model.associationDetails(association).collectionName - @_loadCollectionWithFirstLayer name: newCollectionName, only: association_ids, include: nextLevelInclude, success: (loadedAssociationCollection) => - @_handleNextLayer(loadedAssociationCollection, nextLevelInclude, callback) - callback() - - _loadCollectionWithFirstLayer: (options) => - options = $.extend({}, options) - name = options.name - only = if options.only then _.map((Brainstem.Utils.extractArray "only", options), (id) -> String(id)) else null - search = options.search - include = _(options.include).map((i) -> _.keys(i)[0]) # pull off the top layer of includes - filters = options.filters || {} - order = options.order || "updated_at:desc" - cacheKey = "#{order}|#{_.chain(filters).pairs().map(([k, v]) -> "#{k}:#{v}" ).value().join(",")}|#{options.page}|#{options.perPage}" - - cachedCollection = @storage name - collection = @createNewCollection name, [] - - unless options.cache == false - if only? - alreadyLoadedIds = _.select only, (id) => cachedCollection.get(id)?.associationsAreLoaded(include) - if alreadyLoadedIds.length == only.length - # We've already seen every id that is being asked for and have all the associated data. - @_success options, collection, _.map only, (id) => cachedCollection.get(id) - return collection - else - # Check if we have, at some point, requested enough records with this this order and filter(s). - if @getCollectionDetails(name).cache[cacheKey] - subset = _(@getCollectionDetails(name).cache[cacheKey]).map (result) -> base.data.storage(result.key).get(result.id) - if (_.all(subset, (model) => model.associationsAreLoaded(include))) - @_success options, collection, subset - return collection - - # If we haven't returned yet, we need to go to the server to load some missing data. - syncOptions = - data: {} - parse: true - error: options.error - success: (resp, status, xhr) => - # The server response should look something like this: - # { - # count: 200, - # results: [{ key: "tasks", id: 10 }, { key: "tasks", id: 11 }], - # time_entries: [{ id: 2, title: "te1", project_id: 6, task_id: [10, 11] }] - # projects: [{id: 6, title: "some project", time_entry_ids: [2] }] - # tasks: [{id: 10, title: "some task" }, {id: 11, title: "some other task" }] - # } - # Loop over all returned data types and update our local storage to represent any new data. - - results = resp['results'] - keys = _.reject(_.keys(resp), (key) -> key == 'count' || key == 'results') - unless _.isEmpty(results) - keys.splice(keys.indexOf(name), 1) if keys.indexOf(name) != -1 - keys.push(name) - - for underscoredModelName in keys - @storage(underscoredModelName).update _(resp[underscoredModelName]).values() - - unless options.cache == false || only? - @getCollectionDetails(name).cache[cacheKey] = results - - if only? - @_success options, collection, _.map(only, (id) -> cachedCollection.get(id)) - else - @_success options, collection, _(results).map (result) -> base.data.storage(result.key).get(result.id) - - - syncOptions.data.include = include.join(",") if include.length - syncOptions.data.only = _.difference(only, alreadyLoadedIds).join(",") if only? - syncOptions.data.order = options.order if options.order? - _.extend(syncOptions.data, _(filters).omit('include', 'only', 'order', 'per_page', 'page', 'search')) if _(filters).keys().length - syncOptions.data.per_page = options.perPage unless only? - syncOptions.data.page = options.page unless only? - syncOptions.data.search = search if search - - Backbone.sync.call collection, 'read', collection, syncOptions - - collection - - _success: (options, collection, data) => - if data - data = data.models if data.models? - collection.setLoaded true, trigger: false - if collection.length - collection.add data - else - collection.reset data - collection.setLoaded true - options.success(collection) if options.success? - - _checkPageSettings: (options) => - options.perPage = options.perPage || 20 - options.perPage = 1 if options.perPage < 1 - options.page = options.page || 1 - options.page = 1 if options.page < 1 - - collectionError: (name) => + collectionError: (name) -> Brainstem.Utils.throwError("Unknown collection #{name} in StorageManager. Known collections: #{_(@collections).keys().join(", ")}") - createNewCollection: (collectionName, models = [], options = {}) => + createNewCollection: (collectionName, models = [], options = {}) -> loaded = options.loaded delete options.loaded collection = new (@getCollectionDetails(collectionName).klass)(models, options) collection.setLoaded(true, trigger: false) if loaded collection - createNewModel: (modelName, options) => + createNewModel: (modelName, options) -> new (@getCollectionDetails(modelName.pluralize()).modelKlass)(options || {}) - _wrapObjects: (array) => - output = [] - _(array).each (elem) => - if elem.constructor == Object - for key, value of elem - o = {} - o[key] = @_wrapObjects(if value instanceof Array then value else [value]) - output.push o - else - o = {} - o[elem] = [] - output.push o - output - - _countRequiredServerRequests: (array, wrapped = false) => - if array?.length - array = @_wrapObjects(array) unless wrapped - sum = 1 - _(array).each (elem) => - sum += @_countRequiredServerRequests(_(elem).values()[0], true) - sum - else - 0 - # Expectations and stubbing - stub: (collectionName, options) => + stub: (collectionName, options = {}) -> if @expectations? expectation = new Brainstem.Expectation(collectionName, options, @) @expectations.push expectation expectation else throw "You must call #enableExpectations on your instance of Brainstem.StorageManager before you can set expectations." - stubImmediate: (collectionName, options) => + stubModel: (modelName, modelId, options = {}) -> + @stub(modelName, $.extend({}, options, only: modelId)) + + stubImmediate: (collectionName, options) -> @stub collectionName, $.extend({}, options, immediate: true) - enableExpectations: => + enableExpectations: -> @expectations = [] - handleExpectations: (name, collection, options) => + handleExpectations: (loader) -> for expectation in @expectations - if expectation.optionsMatch(name, options) - expectation.recordRequest(collection, options) + if expectation.loaderOptionsMatch(loader) + expectation.recordRequest(loader) return - throw "No expectation matched #{name} with #{JSON.stringify options}" + throw "No expectation matched #{name} with #{JSON.stringify loader.originalOptions}" + + # Helpers + loadObject: (name, loadOptions, options = {}) -> + options = $.extend({}, { isCollection: true }, options) + + successCallback = loadOptions.success + loadOptions = _.omit(loadOptions, 'success') + loadOptions = $.extend({}, loadOptions, name: name) + + if options.isCollection + loaderClass = Brainstem.CollectionLoader + else + loaderClass = Brainstem.ModelLoader + + @_checkPageSettings loadOptions + + loader = new loaderClass(storageManager: this) + loader.setup(loadOptions) + loader.done(successCallback) if successCallback? && _.isFunction(successCallback) + + if @expectations? + @handleExpectations(loader) + else + loader.load() + + loader + + _checkPageSettings: (options) -> + if options.limit? && options.limit != '' && options.offset? && options.offset != '' + options.perPage = options.page = undefined + else + options.limit = options.offset = undefined + + @_setDefaultPageSettings(options) + + _setDefaultPageSettings: (options) -> + if options.limit? && options.offset? + options.limit = 1 if options.limit < 1 + options.offset = 0 if options.offset < 0 + else + options.perPage = options.perPage || 20 + options.perPage = 1 if options.perPage < 1 + options.page = options.page || 1 + options.page = 1 if options.page < 1 \ No newline at end of file