UNPKG

origins

Version:

JavaScript client for Origins REST service

394 lines (299 loc) 10.4 kB
/* global define */ define([ 'underscore', 'backbone', 'jquery', 'url-template', 'loglevel', './core' ], function(_, Backbone, $, url, logger, origins) { // Placeholder object for reference. This will be populated with the // collection instances below. var store = {}; var Api = Backbone.Model.extend({ initialize: function(attrs, options) { this.url = options.url; } }); // Returns an event handler bound to a store collection that synchronizes // the contents of passed data into the collection. var createSyncHandler = function(name) { return function(event, obj) { logger.debug('syncing', obj, 'on', event); switch (event) { case 'reset': store[name].set(obj.models, {'remove': false}); break; case 'add': store[name].add(obj); break; case 'remove': store[name].remove(obj); break; } }; }; var relationshipsSyncHandler = createSyncHandler('relationships'), resourcesSyncHandler = createSyncHandler('resources'), collectionsSyncHandler = createSyncHandler('collections'), componentsSyncHandler = createSyncHandler('components'); // Initializes instances to data linked by the model. The // URL function of the instance is set to use the model's // corresponding URL in the `_links` attribute var bindLinkedData = function(model, mapping) { _.each(mapping, function(klass, key) { if (model[key]) { throw new Error('model property "' + key + '" already defined'); } var instance = new klass(); model[key] = instance; instance.url = function() { return model.get('_links')[key].href; }; }); }; // Initializes instances to data nested in this model. // A change event handler is defined to update the instace // on model attribute changes. var bindNestedData = function(model, mapping) { _.each(mapping, function(klass, key) { if (model[key]) { throw new Error('model property "' + key + '" already defined'); } var instance = new klass(model.get(key)); model[key] = instance; // Bind change event handler to parent instance.listenTo(model, 'change:' + key, function(obj, value) { this.set(value); }); }); }; var BaseModel = Backbone.Model.extend({ linked: {}, nested: {}, constructor: function(attributes, options) { var attrs = attributes || {}; options = options || {}; this.cid = _.uniqueId('c'); this.attributes = {}; this.fetched = false; this.fetching = false; this.fetchError = null; this.on('request', function(model, xhr) { this._xhr = xhr; this.fetching = true; }); this.on('sync', function() { this.fetching = false; this.fetched = true; }); this.on('error', function(obj, resp) { this.fetching = false; this.fetchError = resp; }); if (options.collection) this.collection = options.collection; if (options.parse) attrs = this.parse(attrs, options) || {}; attrs = _.defaults({}, attrs, _.result(this, 'defaults')); this.set(attrs, options); this.changed = {}; bindLinkedData(this, _.result(this, 'linked')); bindNestedData(this, _.result(this, 'nested')); this.initialize.apply(this, arguments); }, ensure: function(options) { if (this._xhr) return this._xhr; options = _.extend({reset: true}, options); return this.fetch(options); }, parse: function(attrs) { if (attrs && attrs.timestamp) { attrs.parsedTimestamp = new Date(attrs.timestamp); } return attrs; } }); var BaseCollection = Backbone.Collection.extend({ constructor: function() { this.fetched = false; this.fetching = false; this.fetchError = null; this.on('request', function(collection, xhr) { this._xhr = xhr; this.fetching = true; }); // This appears to be an odd handler, however when a fetch/reset occurs // the reset event triggers before the sync event, so this flag would not // in the correct state if downstream consumers relied on it. The solution // is to update the flags if the collection is in the process of fetching. this.on('reset sync', function() { if (this.fetching) { this.fetching = false; this.fetched = true; } }); this.on('error', function(obj, resp) { this.fetching = false; this.fetchError = resp; }); Backbone.Collection.prototype.constructor.apply(this, arguments); }, ensure: function(options) { if (this._xhr) return this._xhr; options = _.extend({reset: true}, options); return this.fetch(options); } }); var Component = BaseModel.extend({ idAttribute: 'uuid', linked: function() { return { relationships: Relationships, revisions: Components, sources: Components, timeline: BaseCollection }; }, nested: function() { return { properties: BaseModel, path: Components }; }, url: function() { var t = url.parse(origins.urls.component.href); return t.expand({uuid: this.get('uuid')}); } }); var Components = BaseCollection.extend({ model: Component, initialize: function(models, options) { if (options && !options.nosync) { this.on('all', componentsSyncHandler); } }, url: function() { return origins.urls.components.href; }, search: function(query) { // Reference the original URL the first time this is accessed if (!this._url) this._url = this.url; // Abort previous request if present if (this._xhr) this._xhr.abort(); if (query) { var _this = this; this.url = function() { return _.result(_this, '_url') + '?' + $.param({query: query}); }; this.fetch({reset: true}); } else { this.url = this._url; this.reset(this.previousModels); } } }); var Relationship = BaseModel.extend({ idAttribute: 'uuid', nested: { properties: BaseModel }, url: function() { var t = url.parse(origins.urls.relationship.href); return t.expand({uuid: this.get('uuid')}); } }); var Relationships = BaseCollection.extend({ model: Relationship, initialize: function(models, options) { if (options && !options.nosync) { this.on('all', relationshipsSyncHandler); } }, url: function() { return origins.urls.relationships.href; } }); var Resource = BaseModel.extend({ idAttribute: 'uuid', linked: { relationships: Relationships, components: Components }, nested: { properties: BaseModel, component_types: BaseCollection // jshint ignore:line }, initialize: function() { this.component_types.constructor.prototype.comparator = 'count'; // jshint ignore:line }, url: function() { var t = url.parse(origins.urls.resource.href); return t.expand({uuid: this.get('uuid')}); } }); var Resources = BaseCollection.extend({ model: Resource, initialize: function(models, options) { if (options && !options.nosync) { this.on('all', resourcesSyncHandler); } }, url: function() { return origins.urls.resources.href; } }); var Collection = BaseModel.extend({ idAttribute: 'uuid', linked: { resources: Resources }, url: function() { var t = url.parse(origins.urls.collections.href); return t.expand({uuid: this.get('uuid')}); } }); var Collections = BaseCollection.extend({ model: Collection, initialize: function(models, options) { if (options && !options.nosync) { this.on('all', collectionsSyncHandler); } }, url: function() { return origins.urls.collections.href; } }); // Initialize collections caches store.relationships = new Relationships(null, { comparator: 'label', nosync: true }); store.components = new Components(null, { comparator: 'label', nosync: true }); store.resources = new Resources(null, { comparator: 'label', nosync: true }); store.collections = new Collections(null, { comparator: 'label', nosync: true }); // Search results, currently just components store.results = new Components(null, { comparator: 'label' }); return { Api: Api, Collections: Collections, Collection: Collection, Resources: Resources, Resource: Resource, Components: Components, Component: Component, Relationships: Relationships, Relationship: Relationship, store: store }; });