-
-
Save lancejpollard/4689443 to your computer and use it in GitHub Desktop.
| // associating properties from different apis with your model. | |
| // say, by default if the associated fields are blank, it will | |
| // fetch and save them from the service. | |
| // `linkedIn.resume` has similar API meaning to `DS.attr('App.Company')` | |
| adapter('linkedIn') | |
| .resource('resume') | |
| .mixin({ | |
| find: function(criteria, callback) { | |
| // criteria == | |
| /* [ | |
| * ['resume', 'resume'] // only passed to linkedIn adapter | |
| * , ['photoUrl', 'user', 'photoUrl'] // only passed to facebook adapter | |
| * ]] | |
| */ | |
| this.get('http://api.linkedIn.com/resume', function(callback) { | |
| }); | |
| } | |
| }) | |
| adapter('twitter') | |
| .resource('user') | |
| .resource('tweet') | |
| .mixin({ | |
| /* | |
| * [ | |
| * ['user', 'profile_image_url'] | |
| * , ['user', 'email'] | |
| * ] | |
| */ | |
| find: function(criteria) { | |
| } | |
| }) | |
| adapter('facebook') | |
| .resource('user') | |
| .resource('status') | |
| .resource('page') | |
| .resource('app') | |
| .mixin({ | |
| /* | |
| * [['user', 'photoUrl']] | |
| */ | |
| find: function(criteria) { | |
| } | |
| }) | |
| model('user') | |
| .field('resume', 'linkedIn.resume') | |
| .field('photoUrl', 'facebook.user.photoUrl') | |
| .field('photoUrl', { | |
| // in this case it may try facebook first, then twitter if nothing found | |
| sources: [ | |
| 'facebook.user.photoUrl' | |
| , 'twitter.user.profile_image_url' | |
| ] | |
| }) | |
| .field('photoUrl', { | |
| // or maybe they need to be, themselves, controllers (or, better, criteria) | |
| sources: [ | |
| criteria('facebook.user').select('photoUrl') | |
| , criteria('twitter.user').select('profile_image_url') | |
| ] | |
| }) | |
| // since `twitter.user` is used more than once, the adapter will have the `twitter` | |
| // object cached in there so it won't need to make another call. | |
| .field('email', 'twitter.user.email') | |
| .index('id', {in: 'redis'}) | |
| // somehow you'd have to know what to pass to the adapter | |
| // (facebook/linkedin credentials, etc.) | |
| controller('user').create(function(error, user) { | |
| assert(user.get('resume')); | |
| assert(user.get('photoUrl')); | |
| }); |
Still figuring out the final end pieces. But I think the biggest thing is mentally breaking away from the way Rails used controllers (that's why I'm liking writing in lowercase like controller('users'), it makes the controllers feel lighter and I can step out of the Rails mindset). Instead, controllers are basically representations of the data; controllers or the ways you manipulate/query/compute/create/destroy data. In this new sense, controllers have arbitrary names, such as controller('unreadMessagesCount'), but there are also default controller classes that are created for (1) every route in the system (e.g. controller('users.index') for route('/users', 'users.index')), (2) every model in the system (e.g. controller('user')) which I'm thinking as of now will only really be used on the client for formatting the field values (that was the i18n stuff earlier today), and (3) every model plural (collection) in the system (e.g. controller('users')). There seems like there some overlap (i.e. controller('users.index') and controller('users') seem like they should probably do the same thing). But, this is where the convention for using controllers comes in. Since there is a controller for every route, you should choose controller('users.index') over controller('users') because the route-specific controller comes packed with extra stuff specific to the route. For example, you can specify specific parameters to be parsed on that route, and if they are found, to be appended to the route's controller's criteria. An example is:route('posts.index').param('title').setup(function(controller) { controller.all() })..., where that controller.all() call will be passing criteria to the store that contains whatever the query parameter value was for "title", such as ['field', 'title', 'match', 'the passed value']). In addition, the criteria passed to the store from controller.all() could also contain user-specific conditions, such as authorization rules for what the user can see. This is the reason you'd pick route-specific controllers over the model-plural controllers like controller('posts'). For each of these route-specific controllers like controller('posts.index') and controller('posts.create'), the first clause in the criteria is going to be ['start', modelName], which tells the adapter what table/collection to use. Now for controllers like controller('unreadMessagesCount'), which don't map to a specific model (at least we can't infer that based on the name), you have to specify the start criteria (start/extend), so realistically it would look more like controller('unreadMessagesCount').start('messages').where('read', false). This is how graph query languages work, specifying the "start node". Then that returns('count') tells the controller to return an integer rather than a record (or array of records). So, this controller doesn't map neatly to a model -- this is why removing the ideas of Rails controllers is helpful -- the controller, instead, is just a representation of the data, a controller for the data.
This also means the idea of a model is more flexible than Rails. Models are basically your database tables, but they're there more to just save your data in an easy-to-query and easy-to-represent way. The controllers are the interface to the models, and so, the only time you ever use model('user') is in defining the model -- creating the schema. To access the model, you use a controller. This is very different than Rails, where you access data from the model as in User.all, but it offers much much more power.
The center of the app, then, is the idea of "resources" more than it is "models". You need models to structure your data so you can persist/query it easily. But you define a set of "routes" for the user/client, and you use controllers to optimally feed different representations of the models to the client.
Tower.Store and Tower.Adapter are going to be like ember-data's DS.Store and DS.Adapter. The "store" is the thing that holds all of the records in your database (for the client, this is in memory, or if you use a localstorage Tower.Adapter, then localstorage; for the server w/ mongodb, it doesn't store the records in memory, it just wraps mongodb). The "adapter" is the key thing on the store, there is a mongodb adapter, a sqlite adapter, etc.. The adapter has methods "find/all", "create/insert", "update", "destroy", and the first parameter is
criteria. Thecriteriais an array of simple statements/clauses (https://gist.github.com/4689178#file-usage-js-L17-L24), which the database-specificadapterunpacks to figure out how to operate on the database (create one/many records, query the database, etc.). Thecriteriagets passed to thestore#findorstore#createmethod, which just passes it off toadapter#findoradapter#createfor database-specific handling. Thecontrolleris what the user users, in the example ofcontroller('messages').where('read', false). Every time you docontroller.where.gte.sort.near.etcyou are just appending clauses to thecontroller.criteriaobject (so, controller has a propertycriteria, starts as a blank array). When you finally call one of the key "exec" methods (find/all/create/update/destroy/count/exists), the controller delegates those calls to thestore, passing thecriteriaas the first parameter.So:
It gets more complicated for a graph database or more complex associations, where you could do something like
controller('users').outgoing('friends').outgoing('friends').outgoing('comments').where('body', /node.js/)to find friends of friends' comments with the word "node.js" in them. Also imagine that users/friends are all in theuserstable in mysql, butcommentsare in mongodb. So when you callcontroller.allwith that friends.friends.comments criteria, it needs to divide the operations up amongst two different stores: mysql first, then append the results from mysql to the criteria, then pass the criteria to mongodb. Socontroller.criteriaare split based on the stores/adapters that contain their data, and then the controller passes the sub-parts of the criteria to the matching stores.This is where that services thing comes into play: https://gist.github.com/4689443#file-potentialadapterusages-js-L52-L72. When you do
controller('users').create()in that linked example, it buildscriteriabased on themodel('user')field "types" (e.g.model('user').field('photoUrl', 'facebook.user.photoUrl'). Socontroller('users').create()essentially has a "default scope" or set of "default criteria", and the criteria matches 3 different "services" and a "store": facebook, twitter, linkedIn, and mongodb (if that's where the users are stored in this example). A "service" is basically the same thing as an "adapter". Socontroller('users').create()has criteria matching 4 adapters (3 services, and mongodb), and if the current record's properties don't fulfill the criteria, ideally it would tell the facebook/twitter/linkedIn adapter to fetch those properties before it saved.