Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save vire/7368a43e86329bd155d1 to your computer and use it in GitHub Desktop.
Save vire/7368a43e86329bd155d1 to your computer and use it in GitHub Desktop.
/**
* Custom data adapter
*
* Adapter delegates request methods to the entity resolver, rather than a regular ajax request.
* Add/edit/delete methods are suppressed for this implementation but could easily be enabled
*
***/
module.exports = DS.Adapter.extend({
entities: {},
// by default, entities are already in normalized form
serializer: null,
/**
Implement this method in order to provide data associated with a type
@method entitiesForType
@param {Subclass of DS.Model} type
@return {Array}
*/
entitiesForType: function(type) {
if(!this.entities[type.typeKey]){
this.entities[type.typeKey] = Ember.A();
}
return this.entities[type.typeKey];
},
/**
Implement this method in order to query entities data
@method queryEntities
@param {Array} entity
@param {Object} query
@param {Subclass of DS.Model} type
@return {Promise|Array}
*/
queryEntities: function(entities, query, type) {
Ember.assert('Not implemented: You must override the DS.EntityAdapter::queryEntities method to support querying the entity store.');
},
/**
@method updateEntities
@param {Subclass of DS.Model} type
@param {Array} entity
*/
updateEntities: function(type, entity) {
if(!this.entities[type.typeKey]){
this.entities[type.typeKey] = Ember.A();
}
var entities = this.entities[type.typeKey];
this.deleteLoadedEntity(type, entity);
entities.push(entity);
},
/**
Implement this method in order to provide json for CRUD methods
@method mockJSON
@param {Subclass of DS.Model} type
@param {DS.Model} record
*/
mockJSON: function(store, type, record) {
return store.serializerFor(type).serialize(record, { includeId: true });
},
/**
@method generateIdForRecord
@param {DS.Store} store
@param {DS.Model} record
@return {String} id
*/
generateIdForRecord: function(store) {
return "entity-" + counter++;
},
/**
@method find
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {String} id
@return {Promise} promise
*/
find: function(store, type, url) {
var entities = this.entitiesForType(type),
entity;
if (entities) {
entity = Ember.A(entities).findProperty('id', url);
}
if (entity) {
return this.getPromise(function() {
return entity;
}, this);
}
var resolver = this.container.lookup('resolver:entity');
return resolver.requestContent(url).then(function(data){
return data;
});
},
/**
@method findMany
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {Array} ids
@return {Promise} promise
*/
findMany: function(store, type, ids) {
var entities = this.entitiesForType(type);
Ember.assert("Unable to find entities for model type "+type.toString(), entities);
if (entities) {
entities = entities.filter(function(item) {
return indexOf(ids, item.id) !== -1;
});
}
if (entities) {
return this.getPromise(function() {
return entities;
}, this);
}
},
/**
@private
@method findAll
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {String} sinceToken
@return {Promise} promise
*/
findAll: function(store, type) {
var entities = this.entitiesForType(type);
Ember.assert("Unable to find entities for model type "+type.toString(), entities);
return this.getPromise(function() {
return entities;
}, this);
},
/**
@private
@method findQuery
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {Object} query
@param {DS.AdapterPopulatedRecordArray} recordArray
@return {Promise} promise
*/
findQuery: function(store, type, query, array) {
var url = query.url;
Ember.assert("No url passed to findQuery(), pass {url: '/the/url'}", url);
var resolver = this.container.lookup('resolver:entity');
return resolver.requestContent(url, query.callback).then(function(data){
return data;
}, function(){
Ember.assert("Unable to find entities for model type " + type.toString());
});
},
/**
@method createRecord
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {DS.Model} record
@return {Promise} promise
*/
createRecord: function(store, type, record) {
var entity = this.mockJSON(store, type, record);
this.updateEntities(type, entity);
return this.getPromise(function() {
return entity;
}, this);
},
/**
@method updateRecord
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {DS.Model} record
@return {Promise} promise
*/
updateRecord: function(store, type, record) {
var entity = this.mockJSON(store, type, record);
this.updateEntities(type, entity);
return this.getPromise(function() {
return entity;
}, this);
},
/**
@method deleteRecord
@param {DS.Store} store
@param {subclass of DS.Model} type
@param {DS.Model} record
@return {Promise} promise
*/
deleteRecord: function(store, type, record) {
var entity = this.mockJSON(store, type, record);
this.deleteLoadedEntity(type, entity);
return this.getPromise(function() {
// no payload in a deletion
return null;
});
},
/*
@method deleteLoadedEntity
@private
@param type
@param record
*/
deleteLoadedEntity: function(type, record) {
var existingEntity = this.findExistingEntity(type, record);
if(existingEntity) {
var index = indexOf(this.entities[type.typeKey], existingEntity);
this.entities[type.typeKey].splice(index, 1);
return true;
}
},
/*
@method findExistingEntity
@private
@param type
@param record
*/
findExistingEntity: function(type, record) {
var entities = this.entitiesForType(type);
var id = Ember.get(record, 'id');
return this.findEntityById(entities, id);
},
/*
@method findEntityById
@private
@param entities
@param id
*/
findEntityById: function(entities, id) {
return Ember.A(entities).find(function(r) {
if(''+Ember.get(r, 'id') === ''+id) {
return true;
} else {
return false;
}
});
},
/*
@method getPromise
@private
@param callback
@param context
*/
getPromise: function(callback, context) {
var adapter = this;
return new Ember.RSVP.Promise(function(resolve) {
resolve(callback.call(context));
}, "DS: EntityAdapter#getPromise");
}
});
/***
* Entity resolver:
*
* This class is a wrapper for a 3rd party service called Mobify (mobify.com)
* Essentially this class handles interfacing with the mobify library, and formatting the response
* in an ember-friendly way. This includes ensuring the entities have an identifier.
* It also provides a level of caching to ensure requests and re-submitted
**/
var App = require('application');
var Mobify = require('./mobify')
var EntityResolver = Ember.Object.extend({
defaults: {
data: {},
url: null
},
baseLocation: null,
data: {},
init: function(){
if(this.defaults){
if(this.defaults.url && this.defaults.data) this.formatData(this.defaults.data, this.defaults.url);
if(this.defaults.data && this.defaults.data.location) this.baseLocation = this.defaults.data.location;
}
},
request: function (url) {
var url = this.stripDomain(url);
Ember.Logger.info('Requesting url: "'+url);
return new Ember.RSVP.Promise(function (resolve, failure) {
if (this.data[url]){
Ember.Logger.info('URL "'+url+'" previously requested, returning cached data:', this.data[url]);
return resolve(this.data[url]);
}
Mobify.request(url,
function (data) {
Ember.Logger.info('URL "'+url+'" responded with data:', data);
if(data.content) resolve(this.formatData(data, url));
else failure(data);
}.bind(this),
failure
);
}.bind(this));
},
requestContent: function(url, callback) {
return this.request(url).then(callback).then(function (data) {
var rtn = null;
switch (data.content.templateName) {
case 'panels':
rtn = data.content.panels;
break;
case 'article':
rtn = data.content;
break;
}
return rtn;
})
},
getData: function(url){
return this.data[url];
},
formatData: function (data, url) {
//Ensure this data wasn't previously imported
if(this.data[url]) return this.data[url];
switch (data.content.templateName) {
case 'panels':
if (data.content.panels instanceof Array) {
for (var i = 0; i < data.content.panels.length; i++) {
var panel = data.content.panels[i];
panel.id = this.stripDomain(panel.href, data.location);
panel.location = url;
data.content.panels[i] = panel;
}
}
break;
case 'article':
var article = data.content;
article.id = data.location.pathname;
article.href = data.absoluteUrl;
data.content = article;;
break;
}
//Cache the data
this.data[url] = data;
return data;
},
stripDomain: function (url, location) {
location = location || this.baseLocation || {origin: ''};
var quote = function (str) {
return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
};
var regex = new RegExp('^' + quote(location.origin));
return url.replace(regex, '');
}
});
var resolver = EntityResolver.create({defaults: {data:Mobify.evaluatedData, url: document.location.pathname}});
module.exports = resolver;
Ember.Application.initializer({
name: 'entityresolver',
initialize: function(container, application) {
application.register('resolver:entity', resolver, {instantiate: false, singleton: true});
}
});
/**
* Routing setup:
* Here we define a wildcard route that catches all requests, even to the homepage
* In the catchall route model hook, we delegate a call to the entity resolver, which in turn returns data
* on how to render the current route. This data is then used to build a route during runtime, add it to the
* defined routes, so that in future, transitioning to this route will use the pre-built route.
* Once the route is built, we immediately transition to the new route.
* A custom model hook for the new route is defined in order to return the correct data from the store,
* this is the only implementation (client) specific part of this operation.
*
**/
var App = require('application');
//Setup routes
App.Router.map(function() {
this.route('catchAll', { path: '*:' });
});
App.CatchAllRoute = App.ApplicationRoute.extend({
model: function(params, transition)
{
//Get the URL and convert to route name
var url = transition.intent.url;
var route_name = Ember.String.classify(url.replace(/[\.|\-|\/]+/g,'_'));
//Check if route already exists, if so, transition to it
var route = route_name ? this.container.resolve('route:'+route_name) : null;
if(route) return this.transitionTo(route_name);
//Get the loader and load the data for the destination url
var resolver = this.container.lookup('resolver:entity');
return resolver.request(url).then(function(data){
//Get the type from the response
var type = data.type;
//If no route is set (index) then set the route name to the type
if(!route_name) route_name = type;
//Add a new route for the url in question
App.Router.map(function(){
this.resource(route_name, {path: url});
});
//Register a new route, manually setting the controller,template and view names
this.container.register('route:'+route_name, App.ApplicationRoute.extend({
controllerName: type,
viewName: type,
templateName: type,
url: url,
model: function(){
var plural = type.substr(-1) == 's'; //Simple plurality check,
var name = plural ? type.substr(0, type.length-1) : type;
//If not plural, do a find by url (id). Model objects are indexed by id
if(!plural) return this.store.find(name, url);
//If plural, make a dyanamic filter
return this.store.filter(name, {url: url}, function(entity){
var match = entity.get('location') == url;
return match;
}.bind(this));
}
}));
//Transition to new route
return this.transitionTo(route_name);
}.bind(this), function(data){
//Force a manual page change
document.location.href = url;
});
}
});
App.IndexRoute = App.CatchAllRoute.extend();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment