Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit aec4fba1ea5308a5ae295a05f286549213b28920 @dudleyf dudleyf committed Mar 8, 2012
Showing with 32,054 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +2 −0 .rspec
  3. +4 −0 Gemfile
  4. 0 README.md
  5. +6 −0 Rakefile
  6. +5 −0 bin/ember
  7. +25 −0 ember.gemspec
  8. +5 −0 lib/ember.rb
  9. +25 −0 lib/ember/app_generator.rb
  10. +13 −0 lib/ember/cli.rb
  11. +3 −0 lib/ember/templates/app/.gitignore
  12. +125 −0 lib/ember/templates/app/Assetfile.tt
  13. +14 −0 lib/ember/templates/app/Gemfile
  14. +13 −0 lib/ember/templates/app/LICENSE
  15. +34 −0 lib/ember/templates/app/README.md
  16. +3,496 −0 lib/ember/templates/app/app/css/bootstrap.css
  17. +7 −0 lib/ember/templates/app/app/css/main.css
  18. +8 −0 lib/ember/templates/app/app/lib/core.js.tt
  19. +19 −0 lib/ember/templates/app/app/lib/ext.js.tt
  20. +7 −0 lib/ember/templates/app/app/lib/main.js.tt
  21. +10 −0 lib/ember/templates/app/app/lib/routes.js.tt
  22. +11 −0 lib/ember/templates/app/app/lib/state_manager.js.tt
  23. +9 −0 lib/ember/templates/app/app/lib/states/start.js.tt
  24. +3 −0 lib/ember/templates/app/app/lib/store.js.tt
  25. +64 −0 lib/ember/templates/app/app/plugins/loader.js
  26. BIN lib/ember/templates/app/app/static/img/glyphicons-halflings-white.png
  27. BIN lib/ember/templates/app/app/static/img/glyphicons-halflings.png
  28. +1 −0 lib/ember/templates/app/app/templates/main_page.handlebars.tt
  29. +2,217 −0 lib/ember/templates/app/app/vendor/ember-data.js
  30. +15,914 −0 lib/ember/templates/app/app/vendor/ember.js
  31. +9,266 −0 lib/ember/templates/app/app/vendor/jquery.js
  32. +570 −0 lib/ember/templates/app/app/vendor/sproutcore-routing.js
  33. +17 −0 lib/ember/templates/app/config.ru
  34. +23 −0 lib/ember/templates/app/index.html.tt
  35. +3 −0 lib/ember/version.rb
  36. +86 −0 spec/ember/cli_spec.rb
  37. +44 −0 spec/spec_helper.rb
5 .gitignore
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+spec/tmp/
2 .rspec
@@ -0,0 +1,2 @@
+--colour
+--format doc
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in ember.gemspec
+gemspec
0 README.md
No changes.
6 Rakefile
@@ -0,0 +1,6 @@
+require "bundler/gem_tasks"
+
+desc "run the specs"
+task :spec do
+ sh "rspec -cfs spec"
+end
5 bin/ember
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require 'ember/cli'
+
+Ember::CLI.start
25 ember.gemspec
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "ember/version"
+
+Gem::Specification.new do |s|
+ s.name = "ember"
+ s.version = Ember::VERSION
+ s.authors = ["Dudley Flanders", "Devin Torres"]
+ s.email = ["dudley@steambone.org"]
+ s.homepage = ""
+ s.summary = %q{Development tools for Ember.js}
+ s.description = %q{Development tools for Ember.js}
+
+ s.rubyforge_project = "ember"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_dependency 'thor'
+
+ s.add_development_dependency 'rspec'
+ s.add_development_dependency 'pry'
+end
5 lib/ember.rb
@@ -0,0 +1,5 @@
+require "ember/version"
+
+module Ember
+ # Your code goes here...
+end
25 lib/ember/app_generator.rb
@@ -0,0 +1,25 @@
+require 'thor/group'
+
+module Ember
+ class AppGenerator < Thor::Group
+ include Thor::Actions
+
+ source_root File.expand_path('../templates/app', __FILE__)
+
+ argument :path
+
+ def name
+ File.basename(path)
+ end
+
+ def title
+ name.capitalize
+ end
+
+ def create
+ self.destination_root = File.expand_path(path, destination_root)
+ directory '.'
+ end
+ end
+end
+
13 lib/ember/cli.rb
@@ -0,0 +1,13 @@
+require 'thor'
+require 'ember/app_generator'
+
+module Ember
+ class CLI < Thor
+ include Thor::Actions
+
+ desc 'new PATH', 'Create a new Ember application'
+ def new(path)
+ Ember::AppGenerator.start([path])
+ end
+ end
+end
3 lib/ember/templates/app/.gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+tmp/
+assets/
125 lib/ember/templates/app/Assetfile.tt
@@ -0,0 +1,125 @@
+APPNAME = '<%= name %>'
+
+require 'json'
+require 'rake-pipeline-web-filters'
+
+WebFilters = Rake::Pipeline::Web::Filters
+
+class LoaderFilter < WebFilters::MinispadeFilter
+ def generate_output(inputs, output)
+ inputs.each do |input|
+ code = input.read
+ module_id = @module_id_generator.call(input)
+ contents = "function(require) {\n#{code}\n}"
+ ret = "\nloader.register('#{module_id}', #{contents});\n"
+ output.write ret
+ end
+ end
+end
+
+class EmberAssertFilter < Filter
+ def generate_output(inputs, output)
+ inputs.each do |input|
+ result = input.read
+ result.gsub!(/ember_assert\((.*)\);/, '')
+ output.write(result)
+ end
+ end
+end
+
+class HandlebarsFilter < Filter
+ def generate_output(inputs, output)
+ inputs.each do |input|
+ code = input.read.to_json
+ name = File.basename(input.path, '.handlebars')
+ output.write "\nreturn Ember.Handlebars.compile(#{code});\n"
+ end
+ end
+end
+
+output 'assets'
+
+input 'app' do
+ match 'lib/**/*.js' do
+ filter LoaderFilter,
+ :module_id_generator => proc { |input|
+ input.path.sub(/^lib\//, "#{APPNAME}/").sub(/\.js$/, '')
+ }
+
+ if ENV['RAKEP_MODE'] == 'production'
+ filter EmberAssertFilter
+ uglify {|input| input}
+ end
+ concat 'app.js'
+ end
+
+ match 'vendor/**/*.js' do
+ filter LoaderFilter,
+ :module_id_generator => proc { |input|
+ input.path.sub(/^vendor\//, '').sub(/\.js$/, '')
+ }
+
+ if ENV['RAKEP_MODE'] == 'production'
+ filter EmberAssertFilter
+ uglify {|input| input}
+ end
+ concat %w[
+ vendor/jquery.js
+ vendor/ember.js
+ vendor/ember-data.js
+ vendor/sproutcore-routing.js
+ ], 'app.js'
+ end
+
+ match 'modules/**/*.js' do
+ if ENV['RAKEP_MODE'] == 'production'
+ filter EmberAssertFilter
+ uglify {|input| input}
+ end
+ concat 'app.js'
+ end
+
+ match 'plugins/**/*.js' do
+ if ENV['RAKEP_MODE'] == 'production'
+ uglify {|input| input}
+ end
+ concat do |input|
+ input.sub(/plugins\//, '')
+ end
+ end
+
+ match 'templates/**/*.handlebars' do
+ filter HandlebarsFilter
+ filter LoaderFilter,
+ :module_id_generator => proc { |input|
+ input.path.sub(/^templates\//, "#{APPNAME}/~templates/").sub(/\.handlebars$/, '')
+ }
+ if ENV['RAKEP_MODE'] == 'production'
+ uglify {|input| input}
+ end
+ concat 'app.js'
+ end
+
+ match 'css/**/*.css' do
+ if ENV['RAKEP_MODE'] == 'production'
+ yui_css
+ end
+ concat ['bootstrap.css', 'main.css'], 'app.css'
+ end
+
+ match 'css/**/*.scss' do
+ sass
+ if ENV['RAKEP_MODE'] == 'production'
+ yui_css
+ end
+ concat 'app.css'
+ end
+
+ match "static/**/*" do
+ concat do |input|
+ input.sub(/static\//, '')
+ end
+ end
+end
+
+# vim: filetype=ruby
14 lib/ember/templates/app/Gemfile
@@ -0,0 +1,14 @@
+source "http://rubygems.org"
+
+gem "rack"
+gem "rack-rewrite"
+# gem "rack-streaming-proxy"
+
+gem "sass"
+gem "compass"
+
+gem "uglifier"
+gem "yui-compressor"
+
+gem "rake-pipeline", :git => "https://github.com/livingsocial/rake-pipeline.git"
+gem "rake-pipeline-web-filters", :git => "https://github.com/wycats/rake-pipeline-web-filters.git"
13 lib/ember/templates/app/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) Your Name Here
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
34 lib/ember/templates/app/README.md
@@ -0,0 +1,34 @@
+Ember Skeleton
+==============
+
+A skeleton application framework using Ember.js and Rake Pipeline.
+
+Running
+-------
+
+ $ bundle install
+ $ bundle exec rackup
+
+App Structure
+-------------
+
+ ember-skeleton
+ ├── Assetfile - App build file
+ ├── Gemfile - Package dependencies for rakep/rack
+ ├── Gemfile.lock - Here be dragons: don't touch, always include
+ ├── app - App specific code
+ │   ├── css - App CSS or SCSS (.scss)
+ │   ├── lib - App code, *modularized during build*
+ │   ├── modules - Module code, *already modularized*
+ │   ├── plugins - Plugins (e.g. jquery.jsonrpc.js)
+ │   │ └── loader.js - JS module loader
+ │   ├── static - Static files, never touched, copied over during build
+ │   ├── templates - Handlebars templates, *modularized during build*
+ │   └── vendor - Vendor code, *modularized during build*
+ ├── assets - Built out asset files, minified in production
+ │ ├── app.css - Built out app CSS/SCSS
+ │ ├── loader.js - Built out JS module loader
+ │ └── app.js - Built out app JS
+ ├── config.ru - Rack development web server configuration
+ ├── index.html - The app entry point
+ └── tmp - Temporary build files used by rakep
3,496 lib/ember/templates/app/app/css/bootstrap.css
3,496 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
7 lib/ember/templates/app/app/css/main.css
@@ -0,0 +1,7 @@
+body {
+ padding-top: 60px;
+}
+
+a {
+ cursor: pointer;
+}
8 lib/ember/templates/app/app/lib/core.js.tt
@@ -0,0 +1,8 @@
+require('jquery');
+require('ember');
+require('ember-data');
+require('<%= name %>/ext');
+
+App = Ember.Application.create({
+ VERSION: '0.1'
+});
19 lib/ember/templates/app/app/lib/ext.js.tt
@@ -0,0 +1,19 @@
+var get = Ember.get;
+
+Ember.View.reopen({
+ templateForName: function(name, type) {
+ if (!name) { return; }
+
+ var templates = get(this, 'templates'),
+ template = get(templates, name);
+
+ if (!template) {
+ template = require(name);
+ if (!template) {
+ throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name]));
+ }
+ }
+
+ return template;
+ }
+});
7 lib/ember/templates/app/app/lib/main.js.tt
@@ -0,0 +1,7 @@
+require('<%= name %>/core');
+require('<%= name %>/store');
+require('<%= name %>/state_manager');
+require('<%= name %>/routes');
+
+// Ember.routes.wantsHistory = true;
+Ember.routes.add('', App, App.routes.mainRoute);
10 lib/ember/templates/app/app/lib/routes.js.tt
@@ -0,0 +1,10 @@
+require('sproutcore-routing');
+
+require('<%= name %>/core');
+
+App.routes = {
+
+ mainRoute: function(params) {
+ }
+
+};
11 lib/ember/templates/app/app/lib/state_manager.js.tt
@@ -0,0 +1,11 @@
+require('<%= name %>/core');
+require('<%= name %>/states/start');
+
+App.stateManager = Ember.StateManager.create({
+
+ rootElement: '#main',
+ initialState: 'start',
+
+ start: App.StartState
+
+});
9 lib/ember/templates/app/app/lib/states/start.js.tt
@@ -0,0 +1,9 @@
+require('<%= name %>/core');
+
+App.StartState = Ember.ViewState.extend({
+
+ view: Ember.View.extend({
+ templateName: '<%= name %>/~templates/main_page'
+ })
+
+});
3 lib/ember/templates/app/app/lib/store.js.tt
@@ -0,0 +1,3 @@
+require('<%= name %>/core');
+
+App.store = DS.Store.create();
64 lib/ember/templates/app/app/plugins/loader.js
@@ -0,0 +1,64 @@
+(function(window) {
+ function requireWrapper(self) {
+ var require = function() {
+ return self.require.apply(self, arguments);
+ };
+ require.exists = function() {
+ return self.exists.apply(self, arguments);
+ };
+ return require;
+ }
+
+ var Context = function() {
+ return this;
+ };
+
+ var Loader = function() {
+ this.modules = {};
+ this.loaded = {};
+ this.exports = {};
+ return this;
+ };
+
+ Loader.prototype.require = function(name) {
+ if (!this.loaded[name]) {
+ var module = this.modules[name];
+ if (module) {
+ var require = requireWrapper(this);
+ try {
+ this.exports[name] = module.call(new Context(), require);
+ return this.exports[name];
+ } finally {
+ this.loaded[name] = true;
+ }
+ } else {
+ throw "The module '" + name + "' has not been registered";
+ }
+ }
+ return this.exports[name];
+ };
+
+ Loader.prototype.register = function(name, module) {
+ if (this.exists(name)) {
+ throw "The module '"+ "' has already been registered";
+ }
+ this.modules[name] = module;
+ return true;
+ };
+
+ Loader.prototype.unregister = function(name) {
+ var loaded = !!this.loaded[name];
+ if (loaded) {
+ delete this.exports[name];
+ delete this.modules[name];
+ delete this.loaded[name];
+ }
+ return loaded;
+ };
+
+ Loader.prototype.exists = function(name) {
+ return name in this.modules;
+ };
+
+ window.loader = new Loader();
+})(this);
BIN lib/ember/templates/app/app/static/img/glyphicons-halflings-white.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN lib/ember/templates/app/app/static/img/glyphicons-halflings.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 lib/ember/templates/app/app/templates/main_page.handlebars.tt
@@ -0,0 +1 @@
+<h1><%= name %> v{{App.VERSION}}</h1>
2,217 lib/ember/templates/app/app/vendor/ember-data.js
@@ -0,0 +1,2217 @@
+
+(function(exports) {
+window.DS = Ember.Namespace.create();
+
+})({});
+
+
+(function(exports) {
+DS.Adapter = Ember.Object.extend({
+ commit: function(store, commitDetails) {
+ commitDetails.updated.eachType(function(type, array) {
+ this.updateRecords(store, type, array.slice());
+ }, this);
+
+ commitDetails.created.eachType(function(type, array) {
+ this.createRecords(store, type, array.slice());
+ }, this);
+
+ commitDetails.deleted.eachType(function(type, array) {
+ this.deleteRecords(store, type, array.slice());
+ }, this);
+ },
+
+ createRecords: function(store, type, models) {
+ models.forEach(function(model) {
+ this.createRecord(store, type, model);
+ }, this);
+ },
+
+ updateRecords: function(store, type, models) {
+ models.forEach(function(model) {
+ this.updateRecord(store, type, model);
+ }, this);
+ },
+
+ deleteRecords: function(store, type, models) {
+ models.forEach(function(model) {
+ this.deleteRecord(store, type, model);
+ }, this);
+ },
+
+ findMany: function(store, type, ids) {
+ ids.forEach(function(id) {
+ this.find(store, type, id);
+ }, this);
+ }
+});
+})({});
+
+
+(function(exports) {
+DS.fixtureAdapter = DS.Adapter.create({
+ find: function(store, type, id) {
+ var fixtures = type.FIXTURES;
+
+ ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+ if (fixtures.hasLoaded) { return; }
+
+ setTimeout(function() {
+ store.loadMany(type, fixtures);
+ fixtures.hasLoaded = true;
+ }, 300);
+ },
+
+ findMany: function() {
+ this.find.apply(this, arguments);
+ },
+
+ findAll: function(store, type) {
+ var fixtures = type.FIXTURES;
+
+ ember_assert("Unable to find fixtures for model type "+type.toString(), !!fixtures);
+
+ var ids = fixtures.map(function(item, index, self){ return item.id; });
+ store.loadMany(type, ids, fixtures);
+ }
+
+});
+
+})({});
+
+
+(function(exports) {
+/*global jQuery*/
+var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
+
+DS.RESTAdapter = DS.Adapter.extend({
+ createRecord: function(store, type, model) {
+ var root = this.rootForType(type);
+
+ var data = {};
+ data[root] = get(model, 'data');
+
+ this.ajax("/" + this.pluralize(root), "POST", {
+ data: data,
+ success: function(json) {
+ store.didCreateRecord(model, json[root]);
+ }
+ });
+ },
+
+ createRecords: function(store, type, models) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, models);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = models.map(function(model) {
+ return get(model, 'data');
+ });
+
+ this.ajax("/" + this.pluralize(root), "POST", {
+ data: data,
+ success: function(json) {
+ store.didCreateRecords(type, models, json[plural]);
+ }
+ });
+ },
+
+ updateRecord: function(store, type, model) {
+ var id = get(model, 'id');
+ var root = this.rootForType(type);
+
+ var data = {};
+ data[root] = get(model, 'data');
+
+ var url = ["", this.pluralize(root), id].join("/");
+
+ this.ajax(url, "PUT", {
+ data: data,
+ success: function(json) {
+ store.didUpdateRecord(model, json[root]);
+ }
+ });
+ },
+
+ updateRecords: function(store, type, models) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, models);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = models.map(function(model) {
+ return get(model, 'data');
+ });
+
+ this.ajax("/" + this.pluralize(root), "POST", {
+ data: data,
+ success: function(json) {
+ store.didUpdateRecords(models, json[plural]);
+ }
+ });
+ },
+
+ deleteRecord: function(store, type, model) {
+ var id = get(model, 'id');
+ var root = this.rootForType(type);
+
+ var url = ["", this.pluralize(root), id].join("/");
+
+ this.ajax(url, "DELETE", {
+ success: function(json) {
+ store.didDeleteRecord(model);
+ }
+ });
+ },
+
+ deleteRecords: function(store, type, models) {
+ if (get(this, 'bulkCommit') === false) {
+ return this._super(store, type, models);
+ }
+
+ var root = this.rootForType(type),
+ plural = this.pluralize(root);
+
+ var data = {};
+ data[plural] = models.map(function(model) {
+ return get(model, 'id');
+ });
+
+ this.ajax("/" + this.pluralize(root) + "/delete", "POST", {
+ data: data,
+ success: function(json) {
+ store.didDeleteRecords(models);
+ }
+ });
+ },
+
+ find: function(store, type, id) {
+ var root = this.rootForType(type);
+
+ var url = ["", this.pluralize(root), id].join("/");
+
+ this.ajax(url, "GET", {
+ success: function(json) {
+ store.load(type, json[root]);
+ }
+ });
+ },
+
+ findMany: function(store, type, ids) {
+ var root = this.rootForType(type), plural = this.pluralize(root);
+
+ this.ajax("/" + plural, "GET", {
+ data: { ids: ids },
+ success: function(json) {
+ store.loadMany(type, ids, json[plural]);
+ }
+ });
+ },
+
+ findAll: function(store, type) {
+ var root = this.rootForType(type), plural = this.pluralize(root);
+
+ this.ajax("/" + plural, "GET", {
+ success: function(json) {
+ store.loadMany(type, json[plural]);
+ }
+ });
+ },
+
+ findQuery: function(store, type, query, modelArray) {
+ var root = this.rootForType(type), plural = this.pluralize(root);
+
+ this.ajax("/" + plural, "GET", {
+ data: query,
+ success: function(json) {
+ modelArray.load(json[plural]);
+ }
+ });
+ },
+
+ // HELPERS
+
+ plurals: {},
+
+ // define a plurals hash in your subclass to define
+ // special-case pluralization
+ pluralize: function(name) {
+ return this.plurals[name] || name + "s";
+ },
+
+ rootForType: function(type) {
+ if (type.url) { return type.url; }
+
+ // use the last part of the name as the URL
+ var parts = type.toString().split(".");
+ var name = parts[parts.length - 1];
+ return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
+ },
+
+ ajax: function(url, type, hash) {
+ hash.url = url;
+ hash.type = type;
+ hash.dataType = "json";
+
+ jQuery.ajax(hash);
+ }
+});
+
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set;
+
+/**
+ A model array is an array that contains records of a certain type. The model
+ array materializes records as needed when they are retrieved for the first
+ time. You should not create model arrays yourself. Instead, an instance of
+ DS.ModelArray or its subclasses will be returned by your application's store
+ in response to queries.
+*/
+
+DS.ModelArray = Ember.ArrayProxy.extend({
+
+ /**
+ The model type contained by this model array.
+
+ @type DS.Model
+ */
+ type: null,
+
+ // The array of client ids backing the model array. When a
+ // record is requested from the model array, the record
+ // for the client id at the same index is materialized, if
+ // necessary, by the store.
+ content: null,
+
+ // The store that created this model array.
+ store: null,
+
+ // for associations, the model that this association belongs to.
+ parentModel: null,
+
+ init: function() {
+ set(this, 'modelCache', Ember.A([]));
+ this._super();
+ },
+
+ // Overrides Ember.Array's replace method to implement
+ replace: function(index, removed, added) {
+ var parentRecord = get(this, 'parentRecord');
+ var pendingParent = parentRecord && !get(parentRecord, 'id');
+
+ added = added.map(function(item) {
+ ember_assert("You can only add items of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === item.constructor));
+
+ if (pendingParent) { item.send('waitingOn', parentRecord); }
+ return item.get('clientId');
+ });
+
+ this._super(index, removed, added);
+ },
+
+ arrayDidChange: function(array, index, removed, added) {
+ var modelCache = get(this, 'modelCache');
+ modelCache.replace(index, 0, new Array(added));
+
+ this._super(array, index, removed, added);
+ },
+
+ arrayWillChange: function(array, index, removed, added) {
+ this._super(array, index, removed, added);
+
+ var modelCache = get(this, 'modelCache');
+ modelCache.replace(index, removed);
+ },
+
+ objectAtContent: function(index) {
+ var modelCache = get(this, 'modelCache');
+ var model = modelCache.objectAt(index);
+
+ if (!model) {
+ var store = get(this, 'store');
+ var content = get(this, 'content');
+
+ var contentObject = content.objectAt(index);
+
+ if (contentObject !== undefined) {
+ model = store.findByClientId(get(this, 'type'), contentObject);
+ modelCache.replace(index, 1, [model]);
+ }
+ }
+
+ return model;
+ }
+});
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get;
+
+DS.FilteredModelArray = DS.ModelArray.extend({
+ filterFunction: null,
+
+ updateFilter: Ember.observer(function() {
+ var store = get(this, 'store');
+ store.updateModelArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
+ }, 'filterFunction')
+});
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set;
+
+DS.AdapterPopulatedModelArray = DS.ModelArray.extend({
+ query: null,
+ isLoaded: false,
+
+ load: function(array) {
+ var store = get(this, 'store'), type = get(this, 'type');
+
+ var clientIds = store.loadMany(type, array).clientIds;
+
+ this.beginPropertyChanges();
+ set(this, 'content', Ember.A(clientIds));
+ set(this, 'isLoaded', true);
+ this.endPropertyChanges();
+ }
+});
+
+
+})({});
+
+
+(function(exports) {
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
+
+var OrderedSet = Ember.Object.extend({
+ init: function() {
+ this.clear();
+ },
+
+ clear: function() {
+ this.set('presenceSet', {});
+ this.set('list', Ember.NativeArray.apply([]));
+ },
+
+ add: function(obj) {
+ var guid = Ember.guidFor(obj),
+ presenceSet = get(this, 'presenceSet'),
+ list = get(this, 'list');
+
+ if (guid in presenceSet) { return; }
+
+ presenceSet[guid] = true;
+ list.pushObject(obj);
+ },
+
+ remove: function(obj) {
+ var guid = Ember.guidFor(obj),
+ presenceSet = get(this, 'presenceSet'),
+ list = get(this, 'list');
+
+ delete presenceSet[guid];
+ list.removeObject(obj);
+ },
+
+ isEmpty: function() {
+ return getPath(this, 'list.length') === 0;
+ },
+
+ forEach: function(fn, self) {
+ // allow mutation during iteration
+ get(this, 'list').slice().forEach(function(item) {
+ fn.call(self, item);
+ });
+ },
+
+ toArray: function() {
+ return get(this, 'list').slice();
+ }
+});
+
+/**
+ A Hash stores values indexed by keys. Unlike JavaScript's
+ default Objects, the keys of a Hash can be any JavaScript
+ object.
+
+ Internally, a Hash has two data structures:
+
+ `keys`: an OrderedSet of all of the existing keys
+ `values`: a JavaScript Object indexed by the
+ Ember.guidFor(key)
+
+ When a key/value pair is added for the first time, we
+ add the key to the `keys` OrderedSet, and create or
+ replace an entry in `values`. When an entry is deleted,
+ we delete its entry in `keys` and `values`.
+*/
+
+var Hash = Ember.Object.extend({
+ init: function() {
+ set(this, 'keys', OrderedSet.create());
+ set(this, 'values', {});
+ },
+
+ add: function(key, value) {
+ var keys = get(this, 'keys'), values = get(this, 'values');
+ var guid = Ember.guidFor(key);
+
+ keys.add(key);
+ values[guid] = value;
+
+ return value;
+ },
+
+ remove: function(key) {
+ var keys = get(this, 'keys'), values = get(this, 'values');
+ var guid = Ember.guidFor(key), value;
+
+ keys.remove(key);
+
+ value = values[guid];
+ delete values[guid];
+
+ return value;
+ },
+
+ fetch: function(key) {
+ var values = get(this, 'values');
+ var guid = Ember.guidFor(key);
+
+ return values[guid];
+ },
+
+ forEach: function(fn, binding) {
+ var keys = get(this, 'keys'),
+ values = get(this, 'values');
+
+ keys.forEach(function(key) {
+ var guid = Ember.guidFor(key);
+ fn.call(binding, key, values[guid]);
+ });
+ }
+});
+
+DS.Transaction = Ember.Object.extend({
+ init: function() {
+ set(this, 'dirty', {
+ created: Hash.create(),
+ updated: Hash.create(),
+ deleted: Hash.create()
+ });
+ },
+
+ createRecord: function(type, hash) {
+ var store = get(this, 'store');
+
+ return store.createRecord(type, hash, this);
+ },
+
+ add: function(model) {
+ var modelTransaction = get(model, 'transaction'),
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
+
+ ember_assert("Models cannot belong to more than one transaction at a time.", modelTransaction === defaultTransaction);
+
+ set(model, 'transaction', this);
+ },
+
+ modelBecameDirty: function(kind, model) {
+ var dirty = get(get(this, 'dirty'), kind),
+ type = model.constructor;
+
+ var models = dirty.fetch(type);
+
+ models = models || dirty.add(type, OrderedSet.create());
+ models.add(model);
+ },
+
+ modelBecameClean: function(kind, model) {
+ var dirty = get(get(this, 'dirty'), kind),
+ type = model.constructor,
+ defaultTransaction = getPath(this, 'store.defaultTransaction');
+
+ var models = dirty.fetch(type);
+ models.remove(model);
+
+ set(model, 'transaction', defaultTransaction);
+ },
+
+ commit: function() {
+ var dirtyMap = get(this, 'dirty');
+
+ var iterate = function(kind, fn, binding) {
+ var dirty = get(dirtyMap, kind);
+
+ dirty.forEach(function(type, models) {
+ if (models.isEmpty()) { return; }
+
+ var array = [];
+
+ models.forEach(function(model) {
+ model.send('willCommit');
+
+ if (get(model, 'isPending') === false) {
+ array.push(model);
+ }
+ });
+
+ fn.call(binding, type, array);
+ });
+ };
+
+ var commitDetails = {
+ updated: {
+ eachType: function(fn, binding) { iterate('updated', fn, binding); }
+ },
+
+ created: {
+ eachType: function(fn, binding) { iterate('created', fn, binding); }
+ },
+
+ deleted: {
+ eachType: function(fn, binding) { iterate('deleted', fn, binding); }
+ }
+ };
+
+ var store = get(this, 'store');
+ var adapter = get(store, '_adapter');
+ if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
+ else { throw fmt("Adapter is either null or do not implement `commit` method", this); }
+ }
+});
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
+
+var OrderedSet = Ember.Object.extend({
+ init: function() {
+ this.clear();
+ },
+
+ clear: function() {
+ this.set('presenceSet', {});
+ this.set('list', Ember.NativeArray.apply([]));
+ },
+
+ add: function(obj) {
+ var guid = Ember.guidFor(obj),
+ presenceSet = get(this, 'presenceSet'),
+ list = get(this, 'list');
+
+ if (guid in presenceSet) { return; }
+
+ presenceSet[guid] = true;
+ list.pushObject(obj);
+ },
+
+ remove: function(obj) {
+ var guid = Ember.guidFor(obj),
+ presenceSet = get(this, 'presenceSet'),
+ list = get(this, 'list');
+
+ delete presenceSet[guid];
+ list.removeObject(obj);
+ },
+
+ isEmpty: function() {
+ return getPath(this, 'list.length') === 0;
+ },
+
+ forEach: function(fn, self) {
+ get(this, 'list').forEach(function(item) {
+ fn.call(self, item);
+ });
+ }
+});
+
+// Implementors Note:
+//
+// The variables in this file are consistently named according to the following
+// scheme:
+//
+// * +id+ means an identifier managed by an external source, provided inside the
+// data hash provided by that source.
+// * +clientId+ means a transient numerical identifier generated at runtime by
+// the data store. It is important primarily because newly created objects may
+// not yet have an externally generated id.
+// * +type+ means a subclass of DS.Model.
+
+/**
+ The store contains all of the hashes for data models loaded from the server.
+ It is also responsible for creating instances of DS.Model when you request one
+ of these data hashes, so that they can be bound to in your Handlebars templates.
+
+ Create a new store like this:
+
+ MyApp.store = DS.Store.create();
+
+ You can retrieve DS.Model instances from the store in several ways. To retrieve
+ a model for a specific id, use the `find()` method:
+
+ var model = MyApp.store.find(MyApp.Contact, 123);
+
+ By default, the store will talk to your backend using a standard REST mechanism.
+ You can customize how the store talks to your backend by specifying a custom adapter:
+
+ MyApp.store = DS.Store.create({
+ adapter: 'MyApp.CustomAdapter'
+ });
+
+ You can learn more about writing a custom adapter by reading the `DS.Adapter`
+ documentation.
+*/
+DS.Store = Ember.Object.extend({
+
+ /**
+ Many methods can be invoked without specifying which store should be used.
+ In those cases, the first store created will be used as the default. If
+ an application has multiple stores, it should specify which store to use
+ when performing actions, such as finding records by id.
+
+ The init method registers this store as the default if none is specified.
+ */
+ init: function() {
+ if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
+ set(DS, 'defaultStore', this);
+ }
+
+ set(this, 'data', []);
+ set(this, '_typeMap', {});
+ set(this, 'recordCache', []);
+ set(this, 'modelArrays', []);
+ set(this, 'modelArraysByClientId', {});
+ set(this, 'defaultTransaction', this.transaction());
+
+ return this._super();
+ },
+
+ transaction: function() {
+ return DS.Transaction.create({ store: this });
+ },
+
+ modelArraysForClientId: function(clientId) {
+ var modelArrays = get(this, 'modelArraysByClientId');
+ var ret = modelArrays[clientId];
+
+ if (!ret) {
+ ret = modelArrays[clientId] = OrderedSet.create();
+ }
+
+ return ret;
+ },
+
+ /**
+ The adapter to use to communicate to a backend server or other persistence layer.
+
+ This can be specified as an instance, a class, or a property path that specifies
+ where the adapter can be located.
+
+ @property {DS.Adapter|String}
+ */
+ adapter: null,
+
+ _adapter: Ember.computed(function() {
+ var adapter = get(this, 'adapter');
+ if (typeof adapter === 'string') {
+ return getPath(this, adapter, false) || getPath(window, adapter);
+ }
+ return adapter;
+ }).property('adapter').cacheable(),
+
+ clientIdCounter: -1,
+
+ // ....................
+ // . CREATE NEW MODEL .
+ // ....................
+
+ createRecord: function(type, properties, transaction) {
+ properties = properties || {};
+
+ // Create a new instance of the model `type` and put it
+ // into the specified `transaction`. If no transaction is
+ // specified, the default transaction will be used.
+ //
+ // NOTE: A `transaction` is specified when the
+ // `transaction.createRecord` API is used.
+ var record = type._create({
+ store: this,
+ transaction: transaction || get(this, 'defaultTransaction')
+ });
+
+ // Extract the primary key from the `properties` hash,
+ // based on the `primaryKey` for the model type.
+ var id = properties[get(record, 'primaryKey')] || null;
+
+ var hash = {}, clientId;
+
+ // Push the hash into the store. If present, associate the
+ // extracted `id` with the hash.
+ clientId = this.pushHash(hash, id, type);
+
+ record.send('setData', hash);
+
+ var recordCache = get(this, 'recordCache');
+
+ // Now that we have a clientId, attach it to the record we
+ // just created.
+ set(record, 'clientId', clientId);
+
+ // Store the record we just created in the record cache for
+ // this clientId.
+ recordCache[clientId] = record;
+
+ // Set the properties specified on the record.
+ record.setProperties(properties);
+
+ // Update any model arrays. Most notably, add this record to
+ // the model arrays returned by `find(type)` and add it to
+ // any filtered arrays for whom this model passes the filter.
+ this.updateModelArrays(type, clientId, hash);
+
+ return record;
+ },
+
+ // ................
+ // . DELETE MODEL .
+ // ................
+
+ deleteRecord: function(model) {
+ model.send('deleteRecord');
+ },
+
+ // ...............
+ // . FIND MODELS .
+ // ...............
+
+ /**
+ Finds a model by its id. If the data for that model has already been
+ loaded, an instance of DS.Model with that data will be returned
+ immediately. Otherwise, an empty DS.Model instance will be returned in
+ the loading state. As soon as the requested data is available, the model
+ will be moved into the loaded state and all of the information will be
+ available.
+
+ Note that only one DS.Model instance is ever created per unique id for a
+ given type.
+
+ Example:
+
+ var record = MyApp.store.find(MyApp.Person, 1234);
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ */
+ find: function(type, id, query) {
+ if (id === undefined) {
+ return this.findAll(type);
+ }
+
+ if (query !== undefined) {
+ return this.findMany(type, id, query);
+ } else if (Ember.typeOf(id) === 'object') {
+ return this.findQuery(type, id);
+ }
+
+ if (Ember.isArray(id)) {
+ return this.findMany(type, id);
+ }
+
+ var clientId = this.clientIdForId(type, id);
+
+ return this.findByClientId(type, clientId, id);
+ },
+
+ findByClientId: function(type, clientId, id) {
+ var model;
+
+ var recordCache = get(this, 'recordCache');
+ var data = this.clientIdToHashMap(type);
+
+ // If there is already a clientId assigned for this
+ // type/id combination, try to find an existing
+ // model for that id and return. Otherwise,
+ // materialize a new model and set its data to the
+ // value we already have.
+ if (clientId !== undefined) {
+ model = recordCache[clientId];
+
+ if (!model) {
+ // create a new instance of the model in the
+ // 'isLoading' state
+ model = this.materializeRecord(type, clientId);
+
+ // immediately set its data
+ model.send('setData', data[clientId] || null);
+ }
+ } else {
+ clientId = this.pushHash(null, id, type);
+
+ // create a new instance of the model in the
+ // 'isLoading' state
+ model = this.materializeRecord(type, clientId);
+
+ // let the adapter set the data, possibly async
+ var adapter = get(this, '_adapter');
+ if (adapter && adapter.find) { adapter.find(this, type, id); }
+ else { throw fmt("Adapter is either null or does not implement `find` method", this); }
+ }
+
+ return model;
+ },
+
+ /** @private
+ */
+ findMany: function(type, ids, query) {
+ var idToClientIdMap = this.idToClientIdMap(type);
+ var data = this.clientIdToHashMap(type), needed;
+
+ var clientIds = Ember.A([]);
+
+ if (ids) {
+ needed = [];
+
+ ids.forEach(function(id) {
+ var clientId = idToClientIdMap[id];
+ if (clientId === undefined || data[clientId] === undefined) {
+ clientId = this.pushHash(null, id, type);
+ needed.push(id);
+ }
+
+ clientIds.push(clientId);
+ }, this);
+ } else {
+ needed = null;
+ }
+
+ if ((needed && get(needed, 'length') > 0) || query) {
+ var adapter = get(this, '_adapter');
+ if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
+ else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
+ }
+
+ return this.createModelArray(type, clientIds);
+ },
+
+ findQuery: function(type, query) {
+ var array = DS.AdapterPopulatedModelArray.create({ type: type, content: Ember.A([]), store: this });
+ var adapter = get(this, '_adapter');
+ if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
+ else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
+ return array;
+ },
+
+ findAll: function(type) {
+
+ var typeMap = this.typeMapFor(type),
+ findAllCache = typeMap.findAllCache;
+
+ if (findAllCache) { return findAllCache; }
+
+ var array = DS.ModelArray.create({ type: type, content: Ember.A([]), store: this });
+ this.registerModelArray(array, type);
+
+ var adapter = get(this, '_adapter');
+ if (adapter && adapter.findAll) { adapter.findAll(this, type); }
+
+ typeMap.findAllCache = array;
+ return array;
+ },
+
+ filter: function(type, filter) {
+ var array = DS.FilteredModelArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
+
+ this.registerModelArray(array, type, filter);
+
+ return array;
+ },
+
+ // ............
+ // . UPDATING .
+ // ............
+
+ hashWasUpdated: function(type, clientId) {
+ var clientIdToHashMap = this.clientIdToHashMap(type);
+ var hash = clientIdToHashMap[clientId];
+
+ this.updateModelArrays(type, clientId, hash);
+ },
+
+ // ..............
+ // . PERSISTING .
+ // ..............
+
+ commit: function() {
+ var defaultTransaction = get(this, 'defaultTransaction');
+ set(this, 'defaultTransaction', this.transaction());
+
+ defaultTransaction.commit();
+ },
+
+ didUpdateRecords: function(array, hashes) {
+ if (arguments.length === 2) {
+ array.forEach(function(model, idx) {
+ this.didUpdateRecord(model, hashes[idx]);
+ }, this);
+ } else {
+ array.forEach(function(model) {
+ this.didUpdateRecord(model);
+ }, this);
+ }
+ },
+
+ didUpdateRecord: function(model, hash) {
+ if (arguments.length === 2) {
+ var clientId = get(model, 'clientId');
+ var data = this.clientIdToHashMap(model.constructor);
+
+ data[clientId] = hash;
+ model.send('setData', hash);
+ }
+
+ model.send('didCommit');
+ },
+
+ didDeleteRecords: function(array) {
+ array.forEach(function(model) {
+ model.send('didCommit');
+ });
+ },
+
+ didDeleteRecord: function(model) {
+ model.send('didCommit');
+ },
+
+ didCreateRecords: function(type, array, hashes) {
+ var id, clientId, primaryKey = getPath(type, 'proto.primaryKey');
+
+ var idToClientIdMap = this.idToClientIdMap(type);
+ var data = this.clientIdToHashMap(type);
+ var idList = this.idList(type);
+
+ for (var i=0, l=get(array, 'length'); i<l; i++) {
+ var model = array[i], hash = hashes[i];
+ id = hash[primaryKey];
+ clientId = get(model, 'clientId');
+
+ data[clientId] = hash;
+ model.send('setData', hash);
+
+ idToClientIdMap[id] = clientId;
+ idList.push(id);
+
+ model.send('didCommit');
+ }
+ },
+
+ didCreateRecord: function(model, hash) {
+ var type = model.constructor;
+
+ var id, clientId, primaryKey;
+
+ var idToClientIdMap = this.idToClientIdMap(type);
+ var data = this.clientIdToHashMap(type);
+ var idList = this.idList(type);
+
+ // The hash is optional, but if it is not provided, the client must have
+ // provided a primary key.
+
+ primaryKey = getPath(type, 'proto.primaryKey');
+
+ // TODO: Make ember_assert more flexible and convert this into an ember_assert
+ if (hash) {
+ ember_assert("The server must provide a primary key: " + primaryKey, get(hash, primaryKey));
+ } else {
+ ember_assert("The server did not return data, and you did not create a primary key (" + primaryKey + ") on the client", get(get(model, 'data'), primaryKey));
+ }
+
+ // If a hash was provided, index it under the model's client ID
+ // and update the model.
+ if (arguments.length === 2) {
+ id = hash[primaryKey];
+
+ data[clientId] = hash;
+ set(model, 'data', hash);
+ }
+
+ clientId = get(model, 'clientId');
+
+ idToClientIdMap[id] = clientId;
+ idList.push(id);
+
+ model.send('didCommit');
+ },
+
+ recordWasInvalid: function(record, errors) {
+ record.send('becameInvalid', errors);
+ },
+
+ // ................
+ // . MODEL ARRAYS .
+ // ................
+
+ registerModelArray: function(array, type, filter) {
+ var modelArrays = get(this, 'modelArrays');
+
+ modelArrays.push(array);
+
+ this.updateModelArrayFilter(array, type, filter);
+ },
+
+ createModelArray: function(type, clientIds) {
+ var array = DS.ModelArray.create({ type: type, content: clientIds, store: this });
+
+ clientIds.forEach(function(clientId) {
+ var modelArrays = this.modelArraysForClientId(clientId);
+ modelArrays.add(array);
+ }, this);
+
+ return array;
+ },
+
+ updateModelArrayFilter: function(array, type, filter) {
+ var data = this.clientIdToHashMap(type);
+ var allClientIds = this.clientIdList(type), clientId, hash;
+
+ for (var i=0, l=allClientIds.length; i<l; i++) {
+ clientId = allClientIds[i];
+
+ hash = data[clientId];
+
+ if (hash) {
+ this.updateModelArray(array, filter, type, clientId, hash);
+ }
+ }
+ },
+
+ updateModelArrays: function(type, clientId, hash) {
+ var modelArrays = get(this, 'modelArrays'),
+ modelArrayType, filter;
+
+ modelArrays.forEach(function(array) {
+ modelArrayType = get(array, 'type');
+ filter = get(array, 'filterFunction');
+
+ if (type !== modelArrayType) { return; }
+
+ this.updateModelArray(array, filter, type, clientId, hash);
+ }, this);
+ },
+
+ updateModelArray: function(array, filter, type, clientId, hash) {
+ var shouldBeInArray;
+
+ if (!filter) {
+ shouldBeInArray = true;
+ } else {
+ shouldBeInArray = filter(hash);
+ }
+
+ var content = get(array, 'content');
+ var alreadyInArray = content.indexOf(clientId) !== -1;
+
+ var modelArrays = this.modelArraysForClientId(clientId);
+
+ if (shouldBeInArray && !alreadyInArray) {
+ modelArrays.add(array);
+ content.pushObject(clientId);
+ } else if (!shouldBeInArray && alreadyInArray) {
+ modelArrays.remove(array);
+ content.removeObject(clientId);
+ }
+ },
+
+ removeFromModelArrays: function(model) {
+ var clientId = get(model, 'clientId');
+ var modelArrays = this.modelArraysForClientId(clientId);
+
+ modelArrays.forEach(function(array) {
+ var content = get(array, 'content');
+ content.removeObject(clientId);
+ });
+ },
+
+ // ............
+ // . TYPE MAP .
+ // ............
+
+ typeMapFor: function(type) {
+ var ids = get(this, '_typeMap');
+ var guidForType = Ember.guidFor(type);
+
+ var typeMap = ids[guidForType];
+
+ if (typeMap) {
+ return typeMap;
+ } else {
+ return (ids[guidForType] =
+ {
+ idToCid: {},
+ idList: [],
+ cidList: [],
+ cidToHash: {}
+ });
+ }
+ },
+
+ idToClientIdMap: function(type) {
+ return this.typeMapFor(type).idToCid;
+ },
+
+ idList: function(type) {
+ return this.typeMapFor(type).idList;
+ },
+
+ clientIdList: function(type) {
+ return this.typeMapFor(type).cidList;
+ },
+
+ clientIdToHashMap: function(type) {
+ return this.typeMapFor(type).cidToHash;
+ },
+
+ /** @private
+
+ For a given type and id combination, returns the client id used by the store.
+ If no client id has been assigned yet, `undefined` is returned.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ */
+ clientIdForId: function(type, id) {
+ return this.typeMapFor(type).idToCid[id];
+ },
+
+ idForHash: function(type, hash) {
+ var primaryKey = getPath(type, 'proto.primaryKey');
+
+ ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
+ return hash[primaryKey];
+ },
+
+ // ................
+ // . LOADING DATA .
+ // ................
+
+ /**
+ Load a new data hash into the store for a given id and type combination.
+ If data for that model had been loaded previously, the new information
+ overwrites the old.
+
+ If the model you are loading data for has outstanding changes that have not
+ yet been saved, an exception will be thrown.
+
+ @param {DS.Model} type
+ @param {String|Number} id
+ @param {Object} hash the data hash to load
+ */
+ load: function(type, id, hash) {
+ if (hash === undefined) {
+ hash = id;
+ var primaryKey = getPath(type, 'proto.primaryKey');
+ ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
+ id = hash[primaryKey];
+ }
+
+ var data = this.clientIdToHashMap(type);
+ var recordCache = get(this, 'recordCache');
+
+ var clientId = this.clientIdForId(type, id);
+
+ if (clientId !== undefined) {
+ data[clientId] = hash;
+
+ var model = recordCache[clientId];
+ if (model) {
+ model.send('setData', hash);
+ }
+ } else {
+ clientId = this.pushHash(hash, id, type);
+ }
+
+ this.updateModelArrays(type, clientId, hash);
+
+ return { id: id, clientId: clientId };
+ },
+
+ loadMany: function(type, ids, hashes) {
+ var clientIds = Ember.A([]);
+
+ if (hashes === undefined) {
+ hashes = ids;
+ ids = [];
+ var primaryKey = getPath(type, 'proto.primaryKey');
+
+ ids = hashes.map(function(hash) {
+ ember_assert("A data hash was loaded for a model of type " + type.toString() + " but no primary key '" + primaryKey + "' was provided.", !!hash[primaryKey]);
+ return hash[primaryKey];
+ });
+ }
+
+ for (var i=0, l=get(ids, 'length'); i<l; i++) {
+ var loaded = this.load(type, ids[i], hashes[i]);
+ clientIds.pushObject(loaded.clientId);
+ }
+
+ return { clientIds: clientIds, ids: ids };
+ },
+
+ /** @private
+
+ Stores a data hash for the specified type and id combination and returns
+ the client id.
+
+ @param {Object} hash
+ @param {String|Number} id
+ @param {DS.Model} type
+ @returns {Number}
+ */
+ pushHash: function(hash, id, type) {
+ var idToClientIdMap = this.idToClientIdMap(type);
+ var clientIdList = this.clientIdList(type);
+ var idList = this.idList(type);
+ var data = this.clientIdToHashMap(type);
+
+ var clientId = this.incrementProperty('clientIdCounter');
+
+ data[clientId] = hash;
+
+ // if we're creating an item, this process will be done
+ // later, once the object has been persisted.
+ if (id) {
+ idToClientIdMap[id] = clientId;
+ idList.push(id);
+ }
+
+ clientIdList.push(clientId);
+
+ return clientId;
+ },
+
+ // .........................
+ // . MODEL MATERIALIZATION .
+ // .........................
+
+ materializeRecord: function(type, clientId) {
+ var model;
+
+ get(this, 'recordCache')[clientId] = model = type._create({
+ store: this,
+ clientId: clientId,
+ transaction: get(this, 'defaultTransaction')
+ });
+ set(model, 'clientId', clientId);
+ model.send('loadingData');
+ return model;
+ },
+
+ destroy: function() {
+ if (get(DS, 'defaultStore') === this) {
+ set(DS, 'defaultStore', null);
+ }
+
+ return this._super();
+ }
+});
+
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;
+
+var stateProperty = Ember.computed(function(key) {
+ var parent = get(this, 'parentState');
+ if (parent) {
+ return get(parent, key);
+ }
+}).property();
+
+var isEmptyObject = function(object) {
+ for (var name in object) {
+ if (object.hasOwnProperty(name)) { return false; }
+ }
+
+ return true;
+};
+
+DS.State = Ember.State.extend({
+ isLoaded: stateProperty,
+ isDirty: stateProperty,
+ isSaving: stateProperty,
+ isDeleted: stateProperty,
+ isError: stateProperty,
+ isNew: stateProperty,
+ isValid: stateProperty,
+ isPending: stateProperty,
+
+ // For states that are substates of a
+ // DirtyState (updated or created), it is
+ // useful to be able to determine which
+ // type of dirty state it is.
+ dirtyType: stateProperty
+});
+
+var isEmptyObject = function(obj) {
+ for (var prop in obj) {
+ if (!obj.hasOwnProperty(prop)) { continue; }
+ return false;
+ }
+
+ return true;
+};
+
+var setProperty = function(manager, context) {
+ var key = context.key, value = context.value;
+
+ var model = get(manager, 'model'),
+ data = get(model, 'data');
+
+ data[key] = value;
+
+ // At the end of the run loop, notify model arrays that
+ // this record has changed so they can re-evaluate its contents
+ // to determine membership.
+ Ember.run.once(model, model.notifyHashWasUpdated);
+};
+
+// The waitingOn event shares common functionality
+// between the different dirty states, but each is
+// treated slightly differently. This method is exposed
+// so that each implementation can invoke the common
+// behavior, and then implement the behavior specific
+// to the state.
+var waitingOn = function(manager, object) {
+ var model = get(manager, 'model'),
+ pendingQueue = get(model, 'pendingQueue'),
+ objectGuid = guidFor(object);
+
+ var observer = function() {
+ if (get(object, 'id')) {
+ manager.send('doneWaitingOn', object);
+ Ember.removeObserver(object, 'id', observer);
+ }
+ };
+
+ pendingQueue[objectGuid] = [object, observer];
+ Ember.addObserver(object, 'id', observer);
+};
+
+// Implementation notes:
+//
+// Each state has a boolean value for all of the following flags:
+//
+// * isLoaded: The record has a populated `data` property. When a
+// record is loaded via `store.find`, `isLoaded` is false
+// until the adapter sets it. When a record is created locally,
+// its `isLoaded` property is always true.
+// * isDirty: The record has local changes that have not yet been
+// saved by the adapter. This includes records that have been
+// created (but not yet saved) or deleted.
+// * isSaving: The record's transaction has been committed, but
+// the adapter has not yet acknowledged that the changes have
+// been persisted to the backend.
+// * isDeleted: The record was marked for deletion. When `isDeleted`
+// is true and `isDirty` is true, the record is deleted locally
+// but the deletion was not yet persisted. When `isSaving` is
+// true, the change is in-flight. When both `isDirty` and
+// `isSaving` are false, the change has persisted.
+// * isError: The adapter reported that it was unable to save
+// local changes to the backend. This may also result in the
+// record having its `isValid` property become false if the
+// adapter reported that server-side validations failed.
+// * isNew: The record was created on the client and the adapter
+// did not yet report that it was successfully saved.
+// * isValid: No client-side validations have failed and the
+// adapter did not report any server-side validation failures.
+// * isPending: A record `isPending` when it belongs to an
+// association on another record and that record has not been
+// saved. A record in this state cannot be saved because it
+// lacks a "foreign key" that will be supplied by its parent
+// association when the parent record has been created. When
+// the adapter reports that the parent has saved, the
+// `isPending` property on all children will become `false`
+// and the transaction will try to commit the records.
+
+
+// The dirty state is a abstract state whose functionality is
+// shared between the `created` and `updated` states.
+//
+// The deleted state shares the `isDirty` flag with the
+// subclasses of `DirtyState`, but with a very different
+// implementation.
+var DirtyState = DS.State.extend({
+ initialState: 'uncommitted',
+
+ // FLAGS
+ isDirty: true,
+
+ // SUBSTATES
+
+ // When a record first becomes dirty, it is `uncommitted`.
+ // This means that there are local pending changes,
+ // but they have not yet begun to be saved.
+ uncommitted: DS.State.extend({
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ model = get(manager, 'model');
+
+ model.withTransaction(function (t) {
+ t.modelBecameDirty(dirtyType, model);
+ });
+ },
+
+ exit: function(manager) {
+ var model = get(manager, 'model');
+ manager.send('invokeLifecycleCallbacks', model);
+ },
+
+ // EVENTS
+ setProperty: setProperty,
+
+ deleteRecord: function(manager) {
+ manager.goToState('deleted');
+ },
+
+ waitingOn: function(manager, object) {
+ waitingOn(manager, object);
+ manager.goToState('pending');
+ },
+
+ willCommit: function(manager) {
+ manager.goToState('inFlight');
+ }
+ }),
+
+ // Once a record has been handed off to the adapter to be
+ // saved, it is in the 'in flight' state. Changes to the
+ // record cannot be made during this window.
+ inFlight: DS.State.extend({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var dirtyType = get(this, 'dirtyType'),
+ model = get(manager, 'model');
+
+ model.withTransaction(function (t) {
+ t.modelBecameClean(dirtyType, model);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ manager.goToState('loaded');
+ },
+
+ becameInvalid: function(manager, errors) {
+ var model = get(manager, 'model');
+
+ set(model, 'errors', errors);
+ manager.goToState('invalid');
+ },
+
+ setData: function(manager, hash) {
+ var model = get(manager, 'model');
+ set(model, 'data', hash);
+ }
+ }),
+
+ // If a record becomes associated with a newly created
+ // parent record, it will be `pending` until the parent
+ // record has successfully persisted. Once this happens,
+ // this record can use the parent's primary key as its
+ // foreign key.
+ //
+ // If the record's transaction had already started to
+ // commit, the record will transition to the `inFlight`
+ // state. If it had not, the record will transition to
+ // the `uncommitted` state.
+ pending: DS.State.extend({
+ initialState: 'uncommitted',
+
+ // FLAGS
+ isPending: true,
+
+ // SUBSTATES
+
+ // A pending record whose transaction has not yet
+ // started to commit is in this state.
+ uncommitted: DS.State.extend({
+ // EVENTS
+ setProperty: setProperty,
+
+ deleteRecord: function(manager) {
+ var model = get(manager, 'model'),
+ pendingQueue = get(model, 'pendingQueue'),
+ tuple;
+
+ // since we are leaving the pending state, remove any
+ // observers we have registered on other records.
+ for (var prop in pendingQueue) {
+ if (!pendingQueue.hasOwnProperty(prop)) { continue; }
+
+ tuple = pendingQueue[prop];
+ Ember.removeObserver(tuple[0], 'id', tuple[1]);
+ }
+
+ manager.goToState('deleted');
+ },
+
+ willCommit: function(manager) {
+ manager.goToState('committing');
+ },
+
+ doneWaitingOn: function(manager, object) {
+ var model = get(manager, 'model'),
+ pendingQueue = get(model, 'pendingQueue'),
+ objectGuid = guidFor(object);
+
+ delete pendingQueue[objectGuid];
+
+ if (isEmptyObject(pendingQueue)) {
+ manager.send('doneWaiting');
+ }
+ },
+
+ doneWaiting: function(manager) {
+ var dirtyType = get(this, 'dirtyType');
+ manager.goToState(dirtyType + '.uncommitted');
+ }
+ }),
+
+ // A pending record whose transaction has started
+ // to commit is in this state. Since it has not yet
+ // been sent to the adapter, it is not `inFlight`
+ // until all of its dependencies have been committed.
+ committing: DS.State.extend({
+ // FLAGS
+ isSaving: true,
+
+ // EVENTS
+ doneWaitingOn: function(manager, object) {
+ var model = get(manager, 'model'),
+ pendingQueue = get(model, 'pendingQueue'),
+ objectGuid = guidFor(object);
+
+ delete pendingQueue[objectGuid];
+
+ if (isEmptyObject(pendingQueue)) {
+ manager.send('doneWaiting');
+ }
+ },
+
+ doneWaiting: function(manager) {
+ var model = get(manager, 'model'),
+ transaction = get(model, 'transaction');
+
+ // Now that the model is no longer pending, schedule
+ // the transaction to commit.
+ Ember.run.once(transaction, transaction.commit);
+ },
+
+ willCommit: function(manager) {
+ var dirtyType = get(this, 'dirtyType');
+ manager.goToState(dirtyType + '.inFlight');
+ }
+ })
+ }),
+
+ // A record is in the `invalid` state when its client-side
+ // invalidations have failed, or if the adapter has indicated
+ // the the record failed server-side invalidations.
+ invalid: DS.State.extend({
+ // FLAGS
+ isValid: false,
+
+ // EVENTS
+ deleteRecord: function(manager) {
+ manager.goToState('deleted');
+ },
+
+ setProperty: function(manager, context) {
+ setProperty(manager, context);
+
+ var model = get(manager, 'model'),
+ errors = get(model, 'errors'),
+ key = context.key;
+
+ delete errors[key];
+
+ if (isEmptyObject(errors)) {
+ manager.send('becameValid');
+ }
+ },
+
+ becameValid: function(manager) {
+ manager.goToState('uncommitted');
+ }
+ })
+});
+
+var states = {
+ rootState: Ember.State.create({
+ // FLAGS
+ isLoaded: false,
+ isDirty: false,
+ isSaving: false,
+ isDeleted: false,
+ isError: false,
+ isNew: false,
+ isValid: true,
+ isPending: false,
+
+ // SUBSTATES
+
+ // A record begins its lifecycle in the `empty` state.
+ // If its data will come from the adapter, it will
+ // transition into the `loading` state. Otherwise, if
+ // the record is being created on the client, it will
+ // transition into the `created` state.
+ empty: DS.State.create({
+ // EVENTS
+ loadingData: function(manager) {
+ manager.goToState('loading');
+ },
+
+ setData: function(manager, hash) {
+ var model = get(manager, 'model');
+ set(model, 'data', hash);
+ manager.goToState('loaded.created');
+ }
+ }),
+
+ // A record enters this state when the store askes
+ // the adapter for its data. It remains in this state
+ // until the adapter provides the requested data.
+ //
+ // Usually, this process is asynchronous, using an
+ // XHR to retrieve the data.
+ loading: DS.State.create({
+ // TRANSITIONS
+ exit: function(manager) {
+ var model = get(manager, 'model');
+ model.didLoad();
+ },
+
+ // EVENTS
+ setData: function(manager, data) {
+ var model = get(manager, 'model');
+
+ model.beginPropertyChanges();
+ set(model, 'data', data);
+
+ if (data !== null) {
+ manager.send('loadedData');
+ }
+
+ model.endPropertyChanges();
+ },
+
+ loadedData: function(manager) {
+ manager.goToState('loaded');
+ }
+ }),
+
+ // A record enters this state when its data is populated.
+ // Most of a record's lifecycle is spent inside substates
+ // of the `loaded` state.
+ loaded: DS.State.create({
+ initialState: 'saved',
+
+ // FLAGS
+ isLoaded: true,
+
+ // SUBSTATES
+
+ // If there are no local changes to a record, it remains
+ // in the `saved` state.
+ saved: DS.State.create({
+ // EVENTS
+ setProperty: function(manager, context) {
+ setProperty(manager, context);
+ manager.goToState('updated');
+ },
+
+ deleteRecord: function(manager) {
+ manager.goToState('deleted');
+ },
+
+ waitingOn: function(manager, object) {
+ waitingOn(manager, object);
+ manager.goToState('updated.pending');
+ }
+ }),
+
+ // A record is in this state after it has been locally
+ // created but before the adapter has indicated that
+ // it has been saved.
+ created: DirtyState.create({
+ dirtyType: 'created',
+
+ // FLAGS
+ isNew: true,
+
+ // EVENTS
+ invokeLifecycleCallbacks: function(manager, model) {
+ model.didCreate();
+ }
+ }),
+
+ // A record is in this state if it has already been
+ // saved to the server, but there are new local changes
+ // that have not yet been saved.
+ updated: DirtyState.create({
+ dirtyType: 'updated',
+
+ // EVENTS
+ invokeLifecycleCallbacks: function(manager, model) {
+ model.didUpdate();
+ }
+ })
+ }),
+
+ // A record is in this state if it was deleted from the store.
+ deleted: DS.State.create({
+ // FLAGS
+ isDeleted: true,
+ isLoaded: true,
+ isDirty: true,
+
+ // TRANSITIONS
+ enter: function(manager) {
+ var model = get(manager, 'model');
+ var store = get(model, 'store');
+
+ if (store) {
+ store.removeFromModelArrays(model);
+ }
+
+ model.withTransaction(function(t) {
+ t.modelBecameDirty('deleted', model);
+ });
+ },
+
+ // SUBSTATES
+
+ // When a record is deleted, it enters the `start`
+ // state. It will exit this state when the record's
+ // transaction starts to commit.
+ start: DS.State.create({
+ willCommit: function(manager) {
+ manager.goToState('inFlight');
+ }
+ }),
+
+ // After a record's transaction is committing, but
+ // before the adapter indicates that the deletion
+ // has saved to the server, a record is in the
+ // `inFlight` substate of `deleted`.
+ inFlight: DS.State.create({
+ // FLAGS
+ isSaving: true,
+
+ // TRANSITIONS
+ exit: function(stateManager) {
+ var model = get(stateManager, 'model');
+
+ model.withTransaction(function(t) {
+ t.modelBecameClean('deleted', model);
+ });
+ },
+
+ // EVENTS
+ didCommit: function(manager) {
+ manager.goToState('saved');
+ }
+ }),
+
+ // Once the adapter indicates that the deletion has
+ // been saved, the record enters the `saved` substate
+ // of `deleted`.
+ saved: DS.State.create({
+ isDirty: false
+ })
+ }),
+
+ // If the adapter indicates that there was an unknown
+ // error saving a record, the record enters the `error`
+ // state.
+ error: DS.State.create({
+ isError: true
+ })
+ })
+};
+
+DS.StateManager = Ember.StateManager.extend({
+ model: null,
+ initialState: 'rootState',
+ states: states
+});
+
+})({});
+
+
+(function(exports) {
+var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
+