-
-
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')); | |
| }); |
// the route map goes into one file so you can find paths quickly
route('posts.index', { path: '/posts' });
// but then you define what they do here
route('posts.index')
.connect(function(controller, callback) {
controller.stream(callback);
})
.request(function(controller, callback) {
controller.all(callback);
})
.respond(function(controller, callback) {
this.render();
})
route('posts.create')
.request(function(controller, callback) {
controller.create(this.get('params'), callback);
})
.respond(function(controller, callback) {
this.render();
})
route('posts.show')
.request(function(controller, callback) {
controller.find(callback);
})
.respond(function(controller, callback) {
this.render();
})// the route map goes into one file so you can find paths quickly
route('/posts', 'posts');
route('/posts', 'posts.create', 'POST');
route('/posts/:id', 'posts.show');
// but then you define what they do here
route('posts')
.connect(function(controller, callback) {
controller.stream(callback);
})
.request(function(controller, callback) {
controller.all(callback);
})
route('posts.create')
.request(function(controller, callback) {
controller.create(this.get('params'), callback);
})
route('posts.show')
.request(function(controller, callback) {
controller.find(callback);
})var app = require('..').create()
, route = app.route
, model = app.model;
route('/posts', 'posts');
route('/posts', 'posts.create', 'POST');
route('/posts/:id', 'posts.show');
route('posts')
.connect(function(controller, callback) {
controller.stream(callback);
})
.request(function(controller, callback) {
controller.all(callback);
});
route('posts.create')
.request(function(controller, callback) {
controller.create(this.get('params'), callback);
});
route('posts.show')
.request(function(controller, callback) {
controller.find(callback);
});
model('post')
.field('title', 'string')
.field('body', 'text')
.outgoing('user')
.incoming('comments');
model('user')
.incoming('posts')
.incoming('comments');
model('comment')
.outgoing('user')
.outgoing('post');
app.initialize();lazily builds:
App = Tower.Application.create();
App.Router.map(function() {
this.route('/posts', 'posts');
this.route('/posts', 'posts.create', 'POST');
this.route('/posts/:id', 'posts.show');
});
App.PostsRoute = Tower.Route.extend({
connect: function(controller, callback) {
controller.stream(callback);
},
request: function(controller, callback) {
controller.all(callback);
});
});
App.PostsCreateRoute = Tower.Route.extend({
request: function(controller, callback) {
controller.create(callback);
});
});
App.PostsShowRoute = Tower.Route.extend({
request: function(controller, callback) {
controller.find(callback);
});
});
App.Post = Tower.Model.extend({
title: Tower.Model.field('string'),
body: Tower.Model.field('text'),
user: Tower.Model.outgoing('App.User'),
comments: Tower.Model.incoming('App.Comment')
});
App.User = Tower.Model.extend({
posts: Tower.Model.incoming('App.Post'),
comments: Tower.Model.incoming('App.Comment')
});
App.Comment = Tower.Model.extend({
user: Tower.Model.outgoing('App.User'),
post: Tower.Model.outgoing('App.Post')
});
App.initialize();try writing both out by hand and see which one feels better.
This would make it so we could take out ember from the backend, which would probably make things a lot leaner.
Perhaps this is close to a "simplest" example, where the missing things are dynamically generated based on the string keys (App.ApplicationView, App.PostsController, etc.), and tower.package.js has every dependency bundled and minified (and tagged with deployment fingerprint). Also, the route definitions from above would be the default implementation, so it's even shorter:
<!doctype html>
<html>
<head>
<title>Blog</title>
<script src='https://cloud.towerjs.org/tower.package.js'></script>
</head>
<body>
<script type='text/x-handlebars' id='posts'>
<ul>
{{#each post in controller}}
<li>
<a {{action show post}}>{{post.title}}</a>
</li>
{{/each}}
</ul>
</script>
<script type='text/x-handlebars' id='posts.show'>
<h2>{{title}}</h2>
<div class='content'>{{content}}</div>
</script>
<script>
var app = tower.create()
, route = app.route
, model = app.model;
route('/posts', 'posts');
route('/posts/:id', 'posts.show');
model('post')
.field('title', 'string')
.field('body', 'text');
app.initialize();
</script>
</body>
</html>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. The criteria is an array of simple statements/clauses (https://gist.github.com/4689178#file-usage-js-L17-L24), which the database-specific adapter unpacks to figure out how to operate on the database (create one/many records, query the database, etc.). The criteria gets passed to the store#find or store#create method, which just passes it off to adapter#find or adapter#create for database-specific handling. The controller is what the user users, in the example of controller('messages').where('read', false). Every time you do controller.where.gte.sort.near.etc you are just appending clauses to the controller.criteria object (so, controller has a property criteria, 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 the store, passing the criteria as the first parameter.
So:
controller.all(callback) --> controller.store.all(controller.criteria, callback) --> store.all(criteria, callback) --> store.adapter.all(criteria, callback)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 the users table in mysql, but comments are in mongodb. So when you call controller.all with 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. So controller.criteria are 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 builds criteria based on the model('user') field "types" (e.g. model('user').field('photoUrl', 'facebook.user.photoUrl'). So controller('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". So controller('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.
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.
This is all you'd need (where
postmodel gets dynamically created and, since it was undefined, you can save anything to it: