├── VERSION ├── packages └── ember-data │ ├── lib │ ├── ext.js │ ├── core.js │ ├── system │ │ ├── model.js │ │ ├── relationships.js │ │ ├── record_arrays.js │ │ ├── mixins │ │ │ ├── load_promise.js │ │ │ └── mappable.js │ │ ├── record_arrays │ │ │ ├── filtered_record_array.js │ │ │ ├── adapter_populated_record_array.js │ │ │ ├── record_array.js │ │ │ └── many_array.js │ │ ├── relationships │ │ │ ├── has_many.js │ │ │ └── belongs_to.js │ │ ├── application_ext.js │ │ └── model │ │ │ └── attributes.js │ ├── adapters.js │ ├── serializers │ │ ├── rest_serializer.js │ │ └── fixture_serializer.js │ ├── main.js │ ├── ext │ │ └── date.js │ ├── transforms │ │ └── json_transforms.js │ └── adapters │ │ └── fixture_adapter.js │ ├── tests │ ├── unit │ │ ├── serializer_test.js │ │ ├── serializers │ │ │ └── rest_serializer_test.js │ │ ├── record_array_test.js │ │ └── adapter_test.js │ ├── integration │ │ ├── mapping_test.js │ │ ├── embedded │ │ │ ├── embedded_saving_test.js │ │ │ └── embedded_dirtying_test.js │ │ ├── find_test.js │ │ ├── relationships │ │ │ ├── many_to_none_relationships_test.js │ │ │ ├── introspection_test.js │ │ │ ├── one_to_none_relationships_test.js │ │ │ ├── embedded_polymorphic_has_many_test.js │ │ │ ├── inverse_test.js │ │ │ ├── one_to_one_relationships_test.js │ │ │ ├── many_to_many_relationships_test.js │ │ │ └── one_to_many_relationships_test.js │ │ ├── client_id_generation_test.js │ │ ├── queries_test.js │ │ ├── transaction_adapter_test.js │ │ ├── method_aliases_test.js │ │ ├── application_test.js │ │ ├── find_all_test.js │ │ ├── adapters │ │ │ └── basic_adapter │ │ │ │ └── commit_test.js │ │ ├── reload_test.js │ │ ├── per_type_adapters.js │ │ ├── rest_adapter_test.js │ │ ├── belongs_to_test.js │ │ ├── prematerialized_data_test.js │ │ ├── transform_test.js │ │ ├── materialization_tests.js │ │ ├── dirtiness_test.js │ │ ├── fixture_adapter_test.js │ │ └── transactions │ │ │ └── relationships_test.js │ └── helpers.js │ └── package.json ├── docs ├── package.json └── yuidoc.json ├── config.ru ├── .gitignore ├── lib └── ember │ └── data │ └── source.rb ├── project.json ├── Gemfile ├── generators └── license.js ├── ember-dev.yml ├── ember-data-source.gemspec ├── .travis.yml ├── Assetfile ├── LICENSE ├── .jshintrc ├── Gemfile.lock ├── Rakefile ├── README.md ├── tests └── ember_configuration.js └── CONTRIBUTING.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0.pre 2 | -------------------------------------------------------------------------------- /packages/ember-data/lib/ext.js: -------------------------------------------------------------------------------- 1 | require('ember-data/ext/date'); 2 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-docs", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "yuidoc": "git://github.com/wagenet/yuidoc.git" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'ember-dev' 3 | 4 | # This is not ideal 5 | map "/lib" do 6 | run Rack::Directory.new('lib') 7 | end 8 | 9 | run EmberDev::Server.new 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .bundle 3 | tmp/ 4 | tests/source/ 5 | dist/ 6 | 7 | .DS_Store 8 | .project 9 | 10 | .github-upload-token 11 | 12 | tests/ember-data-tests.js 13 | *gem 14 | 15 | docs/build/ 16 | -------------------------------------------------------------------------------- /packages/ember-data/lib/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module data 3 | 4 | @main data 5 | */ 6 | 7 | window.DS = Ember.Namespace.create({ 8 | // this one goes past 11 9 | CURRENT_API_REVISION: 12 10 | }); 11 | -------------------------------------------------------------------------------- /lib/ember/data/source.rb: -------------------------------------------------------------------------------- 1 | module Ember 2 | module Data 3 | module Source 4 | def self.bundled_path_for(distro) 5 | File.expand_path("../../../../dist/#{distro}", __FILE__) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module data 3 | @submodule data-model 4 | */ 5 | 6 | require("ember-data/system/model/model"); 7 | require("ember-data/system/model/states"); 8 | require("ember-data/system/model/attributes"); 9 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-project", 3 | "bpm": "1.0.0", 4 | "dependencies": { 5 | "ember-data": ">= 0" 6 | }, 7 | 8 | "bpm:build": { 9 | "bpm_libs.js": { 10 | "minifier": "uglify-js" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ember-data/tests/unit/serializer_test.js: -------------------------------------------------------------------------------- 1 | var serializer; 2 | 3 | module("DS.Serializer", { 4 | setup: function() { 5 | serializer = DS.Serializer.create(); 6 | }, 7 | 8 | teardown: function() { 9 | serializer.destroy(); 10 | } 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake-pipeline", :git => "https://github.com/livingsocial/rake-pipeline.git" 4 | gem "ember-dev", :git => "https://github.com/emberjs/ember-dev.git", :branch => "master" 5 | 6 | gem "ember-source", "1.0.0.rc1.4" 7 | 8 | gem "aws-sdk", "1.8.5" 9 | 10 | -------------------------------------------------------------------------------- /packages/ember-data/lib/adapters.js: -------------------------------------------------------------------------------- 1 | /** 2 | Adapters included with Ember-Data. 3 | 4 | @module data 5 | @submodule data-adapters 6 | 7 | @main data-adapters 8 | */ 9 | 10 | require("ember-data/adapters/fixture_adapter"); 11 | require("ember-data/adapters/rest_adapter"); 12 | require("ember-data/adapters/basic_adapter"); 13 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/relationships.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module data 3 | @submodule data-relationships 4 | */ 5 | 6 | require("ember-data/system/relationships/belongs_to"); 7 | require("ember-data/system/relationships/has_many"); 8 | require("ember-data/system/relationships/ext"); 9 | require("ember-data/system/relationships/one_to_many_change"); 10 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/record_arrays.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module data 3 | @submodule data-record-array 4 | */ 5 | 6 | require('ember-data/system/record_arrays/record_array'); 7 | require('ember-data/system/record_arrays/filtered_record_array'); 8 | require('ember-data/system/record_arrays/adapter_populated_record_array'); 9 | require('ember-data/system/record_arrays/many_array'); 10 | -------------------------------------------------------------------------------- /generators/license.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Project: Ember Data 3 | // Copyright: ©2011-2012 Tilde Inc. and contributors. 4 | // Portions ©2011 Living Social Inc. and contributors. 5 | // License: Licensed under MIT license (see license.js) 6 | // ========================================================================== 7 | 8 | -------------------------------------------------------------------------------- /ember-dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Ember Data 3 | assetfile: 'Assetfile' 4 | testing_suites: 5 | default: 6 | - "package=all" 7 | all: 8 | - "package=all" 9 | - "package=all&jquery=1.8.0&nojshint=true" 10 | - "package=all&extendprototypes=true&nojshint=true" 11 | - "package=all&extendprototypes=true&jquery=1.8.0&nojshint=true" 12 | - "package=all&dist=build&nojshint=true" 13 | testing_packages: 14 | - ember-data 15 | -------------------------------------------------------------------------------- /docs/yuidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The ember-data API", 3 | "description": "The ember-data API: a data persistence library for Ember.js", 4 | "version": "Revision 12", 5 | "logo": "http://f.cl.ly/items/1A1L432s022u1O1q1V3p/ember%20logo.png", 6 | "url": "https://github.com/emberjs/data", 7 | "options": { 8 | "paths": [ 9 | "../packages/ember-data/lib" 10 | ], 11 | "exclude": "vendor", 12 | "outdir": "./build" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ember-data-source.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'json' 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = "ember-data-source" 6 | gem.authors = ["Yehuda Katz"] 7 | gem.email = ["wycats@gmail.com"] 8 | gem.date = Time.now.strftime("%Y-%m-%d") 9 | gem.summary = %q{ember-data source code wrapper.} 10 | gem.description = %q{ember-data source code wrapper for use with Ruby libs.} 11 | gem.homepage = "https://github.com/emberjs/data" 12 | gem.version = "0.0.5" 13 | 14 | gem.add_dependency "ember-source" 15 | 16 | gem.files = Dir['dist/ember-data*.js'] 17 | gem.files << 'lib/ember/data/source.rb' 18 | end 19 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/mixins/load_promise.js: -------------------------------------------------------------------------------- 1 | var DeferredMixin = Ember.DeferredMixin, // ember-runtime/mixins/deferred 2 | Evented = Ember.Evented, // ember-runtime/mixins/evented 3 | run = Ember.run, // ember-metal/run-loop 4 | get = Ember.get; // ember-metal/accessors 5 | 6 | var LoadPromise = Ember.Mixin.create(Evented, DeferredMixin, { 7 | init: function() { 8 | this._super.apply(this, arguments); 9 | this.one('didLoad', function() { 10 | run(this, 'resolve', this); 11 | }); 12 | 13 | if (get(this, 'isLoaded')) { 14 | this.trigger('didLoad'); 15 | } 16 | } 17 | }); 18 | 19 | DS.LoadPromise = LoadPromise; 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rvm: 3 | - 1.9.3 4 | bundler_args: --without development 5 | before_script: 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | - rake clean 9 | script: rake test[all] 10 | after_success: bundle exec rake publish_build 11 | notifications: 12 | webhooks: http://emberjs-master-builds.herokuapp.com/upload/data 13 | env: 14 | global: 15 | - S3_BUCKET_NAME=builds.emberjs.com 16 | - secure: ! 'S+DIdzEPvqQenk1cFq5UjbkoEKDY4j3E/g+Wlz798xxyTkrKQZxoazLXng8I 17 | 18 | gsxElZtB2kpyUq81gWgZcuygO53mcBuCa4rPIsh0Di6Ik+HDELSFVZ4EN4NK 19 | 20 | z9yP6D7pMY+RnlSvErf3OXSzrxkDcXDxCU4ljBJl1rNBbtAOu5E=' 21 | - secure: ! 'YjnT2cF8K0M2fSkab+PY3j8XzumBrjzeGsAN4jtyw4shqnywFaE68qO1IIjY 22 | 23 | UvaE/CbWMxO/6FszR02gJHaF+YyfU5WAS0ahFFLHuC1twMtQPxi+nScjKZEs 24 | 25 | kLwKiKgRNhindV3WvbUcoiIrmrgBMCiBRRd4eyVBlhbZ8RTo1Ig=' 26 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/record_arrays/filtered_record_array.js: -------------------------------------------------------------------------------- 1 | require("ember-data/system/record_arrays/record_array"); 2 | 3 | /** 4 | @module data 5 | @submodule data-record-array 6 | */ 7 | 8 | var get = Ember.get; 9 | 10 | /** 11 | @class FilteredRecordArray 12 | @namespace DS 13 | @extends DS.RecordArray 14 | @constructor 15 | */ 16 | DS.FilteredRecordArray = DS.RecordArray.extend({ 17 | filterFunction: null, 18 | isLoaded: true, 19 | 20 | replace: function() { 21 | var type = get(this, 'type').toString(); 22 | throw new Error("The result of a client-side filter (on " + type + ") is immutable."); 23 | }, 24 | 25 | updateFilter: Ember.observer(function() { 26 | var store = get(this, 'store'); 27 | store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction')); 28 | }, 'filterFunction') 29 | }); 30 | -------------------------------------------------------------------------------- /Assetfile: -------------------------------------------------------------------------------- 1 | require 'ember-dev' 2 | instance_eval File.read(::EmberDev.support_path.join('Assetfile')) 3 | 4 | distros = { 5 | :full => %w(ember-data) 6 | } 7 | 8 | output "dist" 9 | 10 | distros.each do |name, modules| 11 | name = "ember-data" 12 | 13 | input "dist/modules" do 14 | module_paths = modules.map{|m| "#{m}.js" } 15 | match "{#{module_paths.join(',')}}" do 16 | concat(module_paths){ ["#{name}.js", "#{name}.prod.js"] } 17 | end 18 | 19 | match "#{name}.js" do 20 | filter VersionInfo 21 | end 22 | 23 | # Strip dev code 24 | match "#{name}.prod.js" do 25 | filter(EmberStripDebugMessagesFilter) { ["#{name}.prod.js", "min/#{name}.js"] } 26 | end 27 | 28 | # Minify 29 | match "min/#{name}.js" do 30 | uglify{ "#{name}.min.js" } 31 | filter VersionInfo 32 | filter EmberLicenseFilter 33 | end 34 | end 35 | end 36 | 37 | # vim: filetype=ruby 38 | -------------------------------------------------------------------------------- /packages/ember-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data", 3 | "summary": "Short package description", 4 | "description": "Ember Data is a library for loading records from a persistence layer (such as a JSON API), updating those records, then saving the changes. It provides many of the facilities you'd find in server-side ORMs like ActiveRecord, but is designed specifically for the unique environment of JavaScript in the browser.", 5 | "homepage": "https://github.com/emberjs/data", 6 | "author": "Tom Dale, Yehuda Katz, and contributors", 7 | "version": "0.5.0.pre", 8 | 9 | "directories": { 10 | "lib": "lib" 11 | }, 12 | 13 | "dependencies": { 14 | "spade": "~> 1.0", 15 | "ember-runtime": ">= 0" 16 | }, 17 | "dependencies:development": { 18 | "spade-qunit": "~> 1.0.0" 19 | }, 20 | "bpm:build": { 21 | "bpm_libs.js": { 22 | "files": ["lib"], 23 | "modes": "*" 24 | }, 25 | "ember-data/bpm_tests.js": { 26 | "files": ["tests"], 27 | "modes": ["debug"] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/record_arrays/adapter_populated_record_array.js: -------------------------------------------------------------------------------- 1 | require("ember-data/system/record_arrays/record_array"); 2 | 3 | /** 4 | @module data 5 | @submodule data-record-array 6 | */ 7 | 8 | var get = Ember.get, set = Ember.set; 9 | 10 | /** 11 | @class AdapterPopulatedRecordArray 12 | @namespace DS 13 | @extends DS.RecordArray 14 | @constructor 15 | */ 16 | DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({ 17 | query: null, 18 | 19 | replace: function() { 20 | var type = get(this, 'type').toString(); 21 | throw new Error("The result of a server query (on " + type + ") is immutable."); 22 | }, 23 | 24 | load: function(references) { 25 | this.beginPropertyChanges(); 26 | set(this, 'content', Ember.A(references)); 27 | set(this, 'isLoaded', true); 28 | this.endPropertyChanges(); 29 | 30 | var self = this; 31 | // TODO: does triggering didLoad event should be the last action of the runLoop? 32 | Ember.run.once(function() { 33 | self.trigger('didLoad'); 34 | }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by LivingSocial, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/mapping_test.js: -------------------------------------------------------------------------------- 1 | var ParentAdapter, ChildAdapter, Person, store; 2 | 3 | module("Mapping Attributes", { 4 | setup: function() { 5 | Person = window.Person = DS.Model.extend({ 6 | firstName: DS.attr('string'), 7 | lastName: DS.attr('string') 8 | }); 9 | 10 | ParentAdapter = DS.Adapter.extend(); 11 | ChildAdapter = ParentAdapter.extend(); 12 | }, 13 | 14 | teardown: function() { 15 | window.Person = null; 16 | if (store) { store.destroy(); } 17 | } 18 | }); 19 | 20 | test("Attributes mapped on an adapter class should be used when materializing a record.", function() { 21 | ParentAdapter.map('Person', { 22 | lastName: { key: 'LAST_NAME' } 23 | }); 24 | 25 | ChildAdapter.map('Person', { 26 | firstName: { key: 'FIRST_NAME' } 27 | }); 28 | 29 | store = DS.Store.create({ 30 | adapter: ChildAdapter 31 | }); 32 | 33 | store.load(Person, { 34 | id: 1, 35 | FIRST_NAME: "Chuck", 36 | LAST_NAME: "Testa" 37 | }); 38 | 39 | var chuck = store.find(Person, 1); 40 | chuck.get('data'); 41 | 42 | equal(chuck.get('firstName'), "Chuck", "first name is Chuck"); 43 | equal(chuck.get('lastName'), "Testa", "last name is Testa"); 44 | }); 45 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console", 4 | "Ember", 5 | "DS", 6 | "Handlebars", 7 | "Metamorph", 8 | "ember_assert", 9 | "ember_warn", 10 | "ember_deprecate", 11 | "ember_deprecateFunc", 12 | "require", 13 | "equal", 14 | "asyncTest", 15 | "test", 16 | "raises", 17 | "deepEqual", 18 | "start", 19 | "stop", 20 | "ok", 21 | "strictEqual", 22 | "module", 23 | "expect", 24 | "minispade", 25 | "async", 26 | "invokeAsync", 27 | "jQuery" 28 | ], 29 | 30 | "node" : false, 31 | "es5" : true, 32 | "browser" : true, 33 | 34 | "boss" : true, 35 | "curly": false, 36 | "debug": false, 37 | "devel": false, 38 | "eqeqeq": true, 39 | "evil": true, 40 | "forin": false, 41 | "immed": false, 42 | "laxbreak": false, 43 | "newcap": true, 44 | "noarg": true, 45 | "noempty": false, 46 | "nonew": false, 47 | "nomen": false, 48 | "onevar": false, 49 | "plusplus": false, 50 | "regexp": false, 51 | "undef": true, 52 | "sub": true, 53 | "strict": false, 54 | "white": false 55 | } 56 | -------------------------------------------------------------------------------- /packages/ember-data/lib/serializers/rest_serializer.js: -------------------------------------------------------------------------------- 1 | require('ember-data/serializers/json_serializer'); 2 | 3 | /** 4 | @module data 5 | @submodule data-serializers 6 | */ 7 | 8 | /** 9 | @class RESTSerializer 10 | @constructor 11 | @namespace DS 12 | @extends DS.Serializer 13 | */ 14 | 15 | var get = Ember.get; 16 | 17 | DS.RESTSerializer = DS.JSONSerializer.extend({ 18 | keyForAttributeName: function(type, name) { 19 | return Ember.String.decamelize(name); 20 | }, 21 | 22 | keyForBelongsTo: function(type, name) { 23 | var key = this.keyForAttributeName(type, name); 24 | 25 | if (this.embeddedType(type, name)) { 26 | return key; 27 | } 28 | 29 | return key + "_id"; 30 | }, 31 | 32 | keyForHasMany: function(type, name) { 33 | var key = this.keyForAttributeName(type, name); 34 | 35 | if (this.embeddedType(type, name)) { 36 | return key; 37 | } 38 | 39 | return this.singularize(key) + "_ids"; 40 | }, 41 | 42 | keyForPolymorphicId: function(key) { 43 | return key; 44 | }, 45 | 46 | keyForPolymorphicType: function(key) { 47 | return key.replace(/_id$/, '_type'); 48 | }, 49 | 50 | extractValidationErrors: function(type, json) { 51 | var errors = {}; 52 | 53 | get(type, 'attributes').forEach(function(name) { 54 | var key = this._keyForAttributeName(type, name); 55 | if (json['errors'].hasOwnProperty(key)) { 56 | errors[name] = json['errors'][key]; 57 | } 58 | }, this); 59 | 60 | return errors; 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/embedded/embedded_saving_test.js: -------------------------------------------------------------------------------- 1 | var store, Adapter, adapter; 2 | var Post, Comment, User, App; 3 | var attr = DS.attr; 4 | 5 | module("Embedded Saving", { 6 | setup: function() { 7 | App = Ember.Namespace.create({ name: "App" }); 8 | 9 | Comment = App.Comment = DS.Model.extend({ 10 | title: attr('string'), 11 | post: DS.belongsTo('Post') 12 | }); 13 | 14 | Post = App.Post = DS.Model.extend({ 15 | title: attr('string'), 16 | comments: DS.hasMany(Comment) 17 | }); 18 | 19 | Adapter = DS.RESTAdapter.extend(); 20 | 21 | Adapter.map(Post, { 22 | comments: { embedded: 'always' } 23 | }); 24 | 25 | adapter = Adapter.create(); 26 | 27 | store = DS.Store.create({ 28 | adapter: adapter 29 | }); 30 | }, 31 | 32 | teardown: function() { 33 | store.destroy(); 34 | App.destroy(); 35 | } 36 | }); 37 | 38 | asyncTest("Adding a new embedded record to an unsaved record: Both records use the same POST request.", function() { 39 | adapter.ajax = function(url, type, hash) { 40 | equal(url, '/posts'); 41 | equal(type, 'POST'); 42 | equal(hash.data.post.comments.length, 1); 43 | 44 | setTimeout(function() { 45 | hash.success.call(adapter); 46 | start(); 47 | }); 48 | }; 49 | 50 | var transaction = store.transaction(); 51 | var post = transaction.createRecord(Post, { 52 | title: 'This post is unsaved' 53 | }); 54 | 55 | post.get('comments').createRecord({ title: 'This embedded record is also unsaved' }); 56 | 57 | transaction.commit(); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /packages/ember-data/lib/main.js: -------------------------------------------------------------------------------- 1 | //Copyright (C) 2011 by Living Social, Inc. 2 | 3 | //Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | //this software and associated documentation files (the "Software"), to deal in 5 | //the Software without restriction, including without limitation the rights to 6 | //use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | //of the Software, and to permit persons to whom the Software is furnished to do 8 | //so, subject to the following conditions: 9 | 10 | //The above copyright notice and this permission notice shall be included in all 11 | //copies or substantial portions of the Software. 12 | 13 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | //SOFTWARE. 20 | 21 | /** 22 | Ember Data 23 | @module data 24 | */ 25 | 26 | require("ember-data/core"); 27 | require("ember-data/ext"); 28 | require("ember-data/system/store"); 29 | require("ember-data/system/record_arrays"); 30 | require("ember-data/system/model"); 31 | require("ember-data/system/relationships"); 32 | require("ember-data/system/application_ext"); 33 | require("ember-data/system/serializer"); 34 | require("ember-data/system/adapter"); 35 | require("ember-data/adapters"); 36 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/emberjs/ember-dev.git 3 | revision: 2f17d36959adb2dc7b172fe9c8b34a4b99725507 4 | branch: master 5 | specs: 6 | ember-dev (0.1) 7 | colored 8 | ember-source 9 | execjs 10 | grit 11 | handlebars-source 12 | kicker 13 | rack 14 | rake-pipeline (~> 0.8.0) 15 | rake-pipeline-web-filters (~> 0.7.0) 16 | uglifier 17 | 18 | GIT 19 | remote: https://github.com/livingsocial/rake-pipeline.git 20 | revision: 65b1e744defa208e313703d89f3453447cc103b2 21 | specs: 22 | rake-pipeline (0.8.0) 23 | json 24 | rake (~> 10.0.0) 25 | thor 26 | 27 | GEM 28 | remote: https://rubygems.org/ 29 | specs: 30 | aws-sdk (1.8.5) 31 | json (~> 1.4) 32 | nokogiri (>= 1.4.4) 33 | uuidtools (~> 2.1) 34 | colored (1.2) 35 | diff-lcs (1.2.1) 36 | ember-source (1.0.0.rc1.4) 37 | handlebars-source (>= 1.0.0.rc3, < 1.0.0.rc4) 38 | execjs (1.4.0) 39 | multi_json (~> 1.0) 40 | grit (2.5.0) 41 | diff-lcs (~> 1.1) 42 | mime-types (~> 1.15) 43 | posix-spawn (~> 0.3.6) 44 | handlebars-source (1.0.0.rc.3) 45 | json (1.7.7) 46 | kicker (2.6.1) 47 | listen 48 | listen (0.7.3) 49 | mime-types (1.21) 50 | multi_json (1.7.0) 51 | nokogiri (1.5.9) 52 | posix-spawn (0.3.6) 53 | rack (1.5.2) 54 | rake (10.0.3) 55 | rake-pipeline-web-filters (0.7.0) 56 | rack 57 | rake-pipeline (~> 0.6) 58 | thor (0.17.0) 59 | uglifier (1.3.0) 60 | execjs (>= 0.3.0) 61 | multi_json (~> 1.0, >= 1.0.2) 62 | uuidtools (2.1.3) 63 | 64 | PLATFORMS 65 | ruby 66 | 67 | DEPENDENCIES 68 | aws-sdk (= 1.8.5) 69 | ember-dev! 70 | ember-source (= 1.0.0.rc1.4) 71 | rake-pipeline! 72 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/find_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var Person, store, adapter; 3 | 4 | module("Finding Records", { 5 | setup: function() { 6 | Person = DS.Model.extend({ 7 | updatedAt: DS.attr('string'), 8 | name: DS.attr('string'), 9 | firstName: DS.attr('string'), 10 | lastName: DS.attr('string') 11 | }); 12 | 13 | adapter = DS.Adapter.create(); 14 | store = DS.Store.create({ adapter: adapter }); 15 | }, 16 | 17 | teardown: function() { 18 | adapter.destroy(); 19 | store.destroy(); 20 | } 21 | }); 22 | 23 | test("When a single record is requested, the adapter's find method should be called unless it's loaded.", function() { 24 | expect(2); 25 | 26 | var count = 0; 27 | 28 | adapter.find = function(store, type, id) { 29 | equal(type, Person, "the find method is called with the correct type"); 30 | equal(count, 0, "the find method is only called once"); 31 | 32 | store.load(type, id, { id: 1, name: "Braaaahm Dale" }); 33 | 34 | count++; 35 | }; 36 | 37 | store.find(Person, 1); 38 | store.find(Person, 1); 39 | }); 40 | 41 | test("When a record is requested but has not yet been loaded, its `id` property should be the ID used to request the record.", function() { 42 | adapter.find = Ember.K; 43 | 44 | var record = store.find(Person, 1); 45 | equal(get(record, 'id'), 1, "should report its id while loading"); 46 | }); 47 | 48 | test("When multiple records are requested, the default adapter should call the `find` method once per record if findMany is not implemented", function() { 49 | expect(3); 50 | 51 | var count = 0; 52 | adapter.find = function(store, type, id) { 53 | count++; 54 | 55 | equal(id, count); 56 | }; 57 | 58 | store.findMany(Person, [1,2,3]); 59 | store.findMany(Person, [1,2,3]); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/many_to_none_relationships_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("Many-to-None Relationships", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | 18 | App.Comment = DS.Model.extend({ 19 | body: DS.attr('string'), 20 | }); 21 | 22 | App.Post = DS.Model.extend({ 23 | title: DS.attr('string'), 24 | comments: DS.hasMany(App.Comment) 25 | }); 26 | }, 27 | 28 | teardown: function() { 29 | Ember.run(function() { 30 | store.destroy(); 31 | }); 32 | } 33 | }); 34 | 35 | test("Adding a record to a hasMany relationship should work", function() { 36 | store.load(App.Post, { id: 1, title: "parent" }); 37 | store.load(App.Comment, { id: 2, body: "child" }); 38 | 39 | var post = store.find(App.Post, 1), 40 | comment = store.find(App.Comment, 2); 41 | 42 | post.get('comments').pushObject(comment); 43 | deepEqual(post.get('comments').toArray(), [comment], "post should have the comment added to its comments"); 44 | }); 45 | 46 | test("Removing a record from a hasMany relationship should work", function() { 47 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 48 | store.load(App.Comment, { id: 2, body: "child" }); 49 | store.load(App.Comment, { id: 3, body: "child" }); 50 | 51 | var post = store.find(App.Post, 1), 52 | comment1 = store.find(App.Comment, 2), 53 | comment2 = store.find(App.Comment, 3); 54 | 55 | post.get('comments').removeObject(comment1); 56 | deepEqual(post.get('comments').toArray(), [comment2], "post should have the comment added to its comments"); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/client_id_generation_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var serializer, adapter, store; 3 | var Post, Comment; 4 | 5 | module("Client-side ID Generation", { 6 | setup: function() { 7 | serializer = DS.Serializer.create(); 8 | adapter = DS.Adapter.create({ 9 | serializer: serializer 10 | }); 11 | store = DS.Store.create({ 12 | adapter: adapter 13 | }); 14 | 15 | Comment = DS.Model.extend(); 16 | 17 | Post = DS.Model.extend({ 18 | comments: DS.hasMany(Comment) 19 | }); 20 | 21 | Comment.reopen({ 22 | post: DS.belongsTo(Post) 23 | }); 24 | }, 25 | 26 | teardown: function() { 27 | serializer.destroy(); 28 | adapter.destroy(); 29 | store.destroy(); 30 | } 31 | }); 32 | 33 | test("If an adapter implements the `generateIdForRecord` method, the store should be able to assign IDs without saving to the persistence layer.", function() { 34 | expect(6); 35 | 36 | var idCount = 1; 37 | 38 | adapter.generateIdForRecord = function(passedStore, record) { 39 | equal(store, passedStore, "store is the first parameter"); 40 | 41 | return "id-" + idCount++; 42 | }; 43 | 44 | adapter.createRecord = function(store, type, record) { 45 | if (type === Comment) { 46 | equal(get(record, 'id'), 'id-1', "Comment passed to `createRecord` has 'id-1' assigned"); 47 | } else { 48 | equal(get(record, 'id'), 'id-2', "Post passed to `createRecord` has 'id-2' assigned"); 49 | } 50 | }; 51 | 52 | var comment = store.createRecord(Comment); 53 | var post = store.createRecord(Post); 54 | 55 | equal(get(comment, 'id'), 'id-1', "comment is assigned id 'id-1'"); 56 | equal(get(post, 'id'), 'id-2', "post is assigned id 'id-2'"); 57 | 58 | // Despite client-generated IDs, calling commit() on the store should still 59 | // invoke the adapter's `createRecord` method. 60 | store.commit(); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/introspection_test.js: -------------------------------------------------------------------------------- 1 | var Blog, User, Post; 2 | var lookup, oldLookup; 3 | 4 | module("Relationship Introspection", { 5 | setup: function() { 6 | oldLookup = Ember.lookup; 7 | Ember.lookup = {}; 8 | 9 | User = DS.Model.extend(); 10 | Post = DS.Model.extend(); 11 | Blog = DS.Model.extend({ 12 | admins: DS.hasMany(User), 13 | owner: DS.belongsTo(User), 14 | 15 | posts: DS.hasMany(Post) 16 | }); 17 | }, 18 | 19 | teardown: function() { 20 | Ember.lookup = oldLookup; 21 | } 22 | }); 23 | 24 | test("DS.Model class computed property `relationships` returns a map keyed on types", function() { 25 | var relationships = Ember.get(Blog, 'relationships'); 26 | 27 | var expected = [{ name: 'admins', kind: 'hasMany' }, { name: 'owner', kind: 'belongsTo' }]; 28 | deepEqual(relationships.get(User), expected, "user relationships returns expected array"); 29 | 30 | expected = [{ name: 'posts', kind: 'hasMany' }]; 31 | deepEqual(relationships.get(Post), expected, "post relationships returns expected array"); 32 | }); 33 | 34 | test("DS.Model class computed property `relationships` returns a map keyed on types when types are specified as strings", function() { 35 | Blog = DS.Model.extend({ 36 | admins: DS.hasMany('User'), 37 | owner: DS.belongsTo('User'), 38 | 39 | posts: DS.hasMany('Post') 40 | }); 41 | 42 | Ember.lookup = { 43 | User: DS.Model.extend(), 44 | Post: DS.Model.extend() 45 | }; 46 | 47 | var relationships = Ember.get(Blog, 'relationships'); 48 | 49 | var expected = [{ name: 'admins', kind: 'hasMany' }, { name: 'owner', kind: 'belongsTo' }]; 50 | deepEqual(relationships.get(Ember.lookup.User), expected, "user relationships returns expected array"); 51 | 52 | expected = [{ name: 'posts', kind: 'hasMany' }]; 53 | deepEqual(relationships.get(Ember.lookup.Post), expected, "post relationships returns expected array"); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/ember-data/lib/ext/date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Date.parse with progressive enhancement for ISO 8601 3 | * © 2011 Colin Snover 4 | * Released under MIT license. 5 | */ 6 | 7 | Ember.Date = Ember.Date || {}; 8 | 9 | var origParse = Date.parse, numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ]; 10 | Ember.Date.parse = function (date) { 11 | var timestamp, struct, minutesOffset = 0; 12 | 13 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date Time String Format string 14 | // before falling back to any implementation-specific date parsing, so that’s what we do, even if native 15 | // implementations could be faster 16 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 17 | if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { 18 | // avoid NaN timestamps caused by “undefined” values being passed to Date.UTC 19 | for (var i = 0, k; (k = numericKeys[i]); ++i) { 20 | struct[k] = +struct[k] || 0; 21 | } 22 | 23 | // allow undefined days and months 24 | struct[2] = (+struct[2] || 1) - 1; 25 | struct[3] = +struct[3] || 1; 26 | 27 | if (struct[8] !== 'Z' && struct[9] !== undefined) { 28 | minutesOffset = struct[10] * 60 + struct[11]; 29 | 30 | if (struct[9] === '+') { 31 | minutesOffset = 0 - minutesOffset; 32 | } 33 | } 34 | 35 | timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]); 36 | } 37 | else { 38 | timestamp = origParse ? origParse(date) : NaN; 39 | } 40 | 41 | return timestamp; 42 | }; 43 | 44 | if (Ember.EXTEND_PROTOTYPES === true || Ember.EXTEND_PROTOTYPES.Date) { 45 | Date.parse = Ember.Date.parse; 46 | } 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "ember-dev/tasks" 3 | 4 | directory "tmp" 5 | 6 | file "tmp/ember.js" => "tmp" do 7 | cd "tmp" do 8 | sh "git clone https://github.com/emberjs/ember.js.git" 9 | end 10 | end 11 | 12 | task :update_ember_git => ["tmp/ember.js"] do 13 | cd "tmp/ember.js" do 14 | sh "git fetch origin" 15 | sh "git reset --hard origin/master" 16 | end 17 | end 18 | 19 | file "tmp/ember.js/dist/ember.js" 20 | 21 | file "packages/ember/lib/main.js" => [:update_ember_git, "tmp/ember.js/dist/ember.js"] do 22 | cd "tmp/ember.js" do 23 | sh "rake dist" 24 | cp "dist/ember.js", "../../packages/ember/lib/main.js" 25 | end 26 | end 27 | 28 | task :update_ember => "packages/ember/lib/main.js" 29 | 30 | 31 | task :clean => "ember:clean" 32 | task :dist => "ember:dist" 33 | task :test, [:suite] => "ember:test" 34 | task :default => :dist 35 | 36 | task :publish_build do 37 | require 'date' 38 | require 'aws-sdk' 39 | access_key_id = ENV['S3_ACCESS_KEY_ID'] 40 | secret_access_key = ENV['S3_SECRET_ACCESS_KEY'] 41 | bucket_name = ENV['S3_BUCKET_NAME'] 42 | rev=`git rev-list HEAD -n 1`.to_s.strip 43 | master_rev = `git rev-list origin/master -n 1`.to_s.strip 44 | return unless rev == master_rev 45 | return unless access_key_id && secret_access_key && bucket_name 46 | s3 = AWS::S3.new( 47 | :access_key_id => access_key_id, 48 | :secret_access_key => secret_access_key 49 | ) 50 | bucket = s3.buckets[bucket_name] 51 | ember_data_latest = bucket.objects['ember-data-latest.js'] 52 | ember_data_latest_min = bucket.objects['ember-data-latest.min.js'] 53 | ember_data_dev = bucket.objects["ember-data-#{rev}.js"] 54 | ember_data_min = bucket.objects["ember-data-#{rev}.min.js"] 55 | dist = File.dirname(__FILE__) + '/dist/' 56 | data_path = Pathname.new dist + 'ember-data.js' 57 | min_path = Pathname.new dist + 'ember-data.min.js' 58 | ember_data_dev.write data_path 59 | ember_data_latest.write data_path 60 | ember_data_latest_min.write min_path 61 | ember_data_min.write min_path 62 | puts "Published ember-data for #{rev}" 63 | end 64 | 65 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/one_to_none_relationships_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("One-to-None Relationships", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | 18 | App.Post = DS.Model.extend({ 19 | title: DS.attr('string') 20 | }); 21 | 22 | App.Comment = DS.Model.extend({ 23 | body: DS.attr('string'), 24 | post: DS.belongsTo(App.Post) 25 | }); 26 | 27 | }, 28 | 29 | teardown: function() { 30 | Ember.run(function() { 31 | store.destroy(); 32 | }); 33 | } 34 | }); 35 | 36 | function verifySynchronizedOneToMany(post, comment, expectedHasMany) { 37 | expectedHasMany = expectedHasMany || [comment]; 38 | equal(comment.get('post'), post); 39 | deepEqual(post.get('comments').toArray(), expectedHasMany); 40 | } 41 | 42 | test("Setting a record's belongsTo relationship to another record, should work", function() { 43 | store.load(App.Post, { id: 1, title: "parent" }); 44 | store.load(App.Comment, { id: 2, body: "child" }); 45 | 46 | var post = store.find(App.Post, 1), 47 | comment = store.find(App.Comment, 2); 48 | 49 | comment.set('post', post); 50 | deepEqual(comment.get('post'), post, "comment should have the correct post set"); 51 | }); 52 | 53 | test("Setting a record's belongsTo relationship to null should work", function() { 54 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 55 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 56 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 57 | 58 | var post = store.find(App.Post, 1), 59 | comment1 = store.find(App.Comment, 2), 60 | comment2 = store.find(App.Comment, 3); 61 | 62 | comment1.set('post', null); 63 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/ember-data/tests/helpers.js: -------------------------------------------------------------------------------- 1 | DS.MockObject = Ember.Object.extend({ 2 | init: function() { 3 | this.spyMeta = {}; 4 | }, 5 | 6 | spyOn: function(methodName) { 7 | var func = this[methodName], 8 | meta = this.spyMeta; 9 | 10 | if (func && func.isSpy) { return; } 11 | 12 | if (!func) { 13 | func = function() { }; 14 | } 15 | 16 | this[methodName] = function() { 17 | var callCount = meta[methodName] || 0; 18 | 19 | meta[methodName] = ++callCount; 20 | 21 | return func.apply(this, arguments); 22 | }; 23 | 24 | this[methodName].isSpy = true; 25 | }, 26 | 27 | shouldHaveBeenCalled: function(methodName, times) { 28 | times = times === undefined ? 1 : times; 29 | 30 | equal(this.spyMeta[methodName], times, methodName+" was called "+times+ " times"); 31 | } 32 | }); 33 | 34 | DS.MockModel = DS.MockObject.extend({ 35 | init: function() { 36 | this.resetEvents(); 37 | 38 | return this._super(); 39 | }, 40 | 41 | send: function(event) { 42 | this.receivedEvents.push(event); 43 | }, 44 | 45 | resetEvents: function() { 46 | this.receivedEvents = []; 47 | }, 48 | 49 | didReceiveEvent: function(event) { 50 | return -1 !== this.receivedEvents.indexOf(event); 51 | }, 52 | 53 | shouldHaveReceived: function() { 54 | var events = Array.prototype.slice.call(arguments); 55 | 56 | Ember.ArrayPolyfills.forEach.call(events, function(event) { 57 | ok(this.didReceiveEvent(event), "record received event "+event); 58 | }, this); 59 | }, 60 | 61 | shouldNotHaveReceived: function() { 62 | var events = Array.prototype.slice.call(arguments); 63 | 64 | Ember.ArrayPolyfills.forEach.call(events, function(event) { 65 | ok(!this.didReceiveEvent(event), "record did not received event "+event); 66 | }, this); 67 | }, 68 | 69 | becomeDirty: function(type) { 70 | this.get('transaction').recordBecameDirty(type, this); 71 | }, 72 | 73 | eachAttribute: Ember.K, 74 | eachRelationshipChange: Ember.K, 75 | setupData: Ember.K 76 | }); 77 | 78 | DS.MockModel.reopenClass({ 79 | _create: function() { 80 | return this.create.apply(this, arguments); 81 | } 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/relationships/has_many.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set, forEach = Ember.ArrayPolyfills.forEach; 2 | 3 | require("ember-data/system/model/model"); 4 | 5 | var hasRelationship = function(type, options) { 6 | options = options || {}; 7 | 8 | var meta = { type: type, isRelationship: true, options: options, kind: 'hasMany' }; 9 | 10 | return Ember.computed(function(key, value) { 11 | var data = get(this, 'data').hasMany, 12 | store = get(this, 'store'), 13 | ids, relationship; 14 | 15 | if (typeof type === 'string') { 16 | type = get(this, type, false) || get(Ember.lookup, type); 17 | } 18 | 19 | //ids can be references or opaque token 20 | //(e.g. `{url: '/relationship'}`) that will be passed to the adapter 21 | ids = data[key]; 22 | 23 | relationship = store.findMany(type, ids, this, meta); 24 | set(relationship, 'owner', this); 25 | set(relationship, 'name', key); 26 | set(relationship, 'isPolymorphic', options.polymorphic); 27 | 28 | return relationship; 29 | }).property().meta(meta); 30 | }; 31 | 32 | DS.hasMany = function(type, options) { 33 | Ember.assert("The type passed to DS.hasMany must be defined", !!type); 34 | return hasRelationship(type, options); 35 | }; 36 | 37 | function clearUnmaterializedHasMany(record, relationship) { 38 | var store = get(record, 'store'), 39 | data = get(record, 'data').hasMany; 40 | 41 | var references = data[relationship.key]; 42 | 43 | if (!references) { return; } 44 | 45 | var inverseName = record.constructor.inverseFor(relationship.key).name; 46 | 47 | 48 | forEach.call(references, function(reference) { 49 | var childRecord; 50 | 51 | if (store.isReferenceMaterialized(reference)) { 52 | childRecord = store.recordForReference(reference); 53 | 54 | record.suspendRelationshipObservers(function() { 55 | set(childRecord, inverseName, null); 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | DS.Model.reopen({ 62 | clearHasMany: function(relationship) { 63 | var hasMany = this.cacheFor(relationship.name); 64 | 65 | if (hasMany) { 66 | hasMany.clear(); 67 | } else { 68 | clearUnmaterializedHasMany(this, relationship); 69 | } 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/queries_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var Person, store, adapter; 3 | 4 | module("Queries", { 5 | setup: function() { 6 | var App = Ember.Namespace.create({ name: "App" }); 7 | 8 | Person = App.Person = DS.Model.extend({ 9 | updatedAt: DS.attr('string'), 10 | name: DS.attr('string'), 11 | firstName: DS.attr('string'), 12 | lastName: DS.attr('string') 13 | }); 14 | 15 | adapter = DS.Adapter.create(); 16 | store = DS.Store.create({ adapter: adapter }); 17 | }, 18 | 19 | teardown: function() { 20 | adapter.destroy(); 21 | store.destroy(); 22 | } 23 | }); 24 | 25 | test("When a query is made, the adapter should receive a record array it can populate with the results of the query.", function() { 26 | expect(8); 27 | 28 | adapter.findQuery = function(store, type, query, recordArray) { 29 | equal(type, Person, "the find method is called with the correct type"); 30 | 31 | stop(); 32 | 33 | var self = this; 34 | 35 | // Simulate latency to ensure correct behavior in asynchronous conditions. 36 | // Once 100ms has passed, load the results of the query into the record array. 37 | setTimeout(function() { 38 | Ember.run(function() { 39 | self.didFindQuery(store, type, { persons: [{ id: 1, name: "Peter Wagenet" }, { id: 2, name: "Brohuda Katz" }] }, recordArray); 40 | }); 41 | }, 100); 42 | }; 43 | 44 | var queryResults = store.find(Person, { page: 1 }); 45 | equal(get(queryResults, 'length'), 0, "the record array has a length of zero before the results are loaded"); 46 | equal(get(queryResults, 'isLoaded'), false, "the record array's `isLoaded` property is false"); 47 | 48 | queryResults.one('didLoad', function() { 49 | equal(get(queryResults, 'length'), 2, "the record array has a length of 2 after the results are loaded"); 50 | equal(get(queryResults, 'isLoaded'), true, "the record array's `isLoaded` property should be true"); 51 | 52 | equal(queryResults.objectAt(0).get('name'), "Peter Wagenet", "the first record is 'Peter Wagenet'"); 53 | equal(queryResults.objectAt(1).get('name'), "Brohuda Katz", "the second record is 'Brohuda Katz'"); 54 | }); 55 | 56 | queryResults.then(function(resolvedValue) { 57 | start(); 58 | 59 | equal(resolvedValue, queryResults, "The promise was resolved with the query results"); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/record_arrays/record_array.js: -------------------------------------------------------------------------------- 1 | require("ember-data/system/mixins/load_promise"); 2 | 3 | /** 4 | */ 5 | 6 | var get = Ember.get, set = Ember.set; 7 | 8 | var LoadPromise = DS.LoadPromise; // system/mixins/load_promise 9 | 10 | /** 11 | A record array is an array that contains records of a certain type. The record 12 | array materializes records as needed when they are retrieved for the first 13 | time. You should not create record arrays yourself. Instead, an instance of 14 | DS.RecordArray or its subclasses will be returned by your application's store 15 | in response to queries. 16 | 17 | @module data 18 | @submodule data-record-array 19 | @main data-record-array 20 | 21 | @class RecordArray 22 | @namespace DS 23 | @extends Ember.ArrayProxy 24 | @uses Ember.Evented 25 | @uses DS.LoadPromise 26 | */ 27 | 28 | DS.RecordArray = Ember.ArrayProxy.extend(Ember.Evented, LoadPromise, { 29 | /** 30 | The model type contained by this record array. 31 | 32 | @type DS.Model 33 | */ 34 | type: null, 35 | 36 | // The array of client ids backing the record array. When a 37 | // record is requested from the record array, the record 38 | // for the client id at the same index is materialized, if 39 | // necessary, by the store. 40 | content: null, 41 | 42 | isLoaded: false, 43 | isUpdating: false, 44 | 45 | // The store that created this record array. 46 | store: null, 47 | 48 | objectAtContent: function(index) { 49 | var content = get(this, 'content'), 50 | reference = content.objectAt(index), 51 | store = get(this, 'store'); 52 | 53 | if (reference) { 54 | return store.recordForReference(reference); 55 | } 56 | }, 57 | 58 | materializedObjectAt: function(index) { 59 | var reference = get(this, 'content').objectAt(index); 60 | if (!reference) { return; } 61 | 62 | if (get(this, 'store').recordIsMaterialized(reference)) { 63 | return this.objectAt(index); 64 | } 65 | }, 66 | 67 | update: function() { 68 | if (get(this, 'isUpdating')) { return; } 69 | 70 | var store = get(this, 'store'), 71 | type = get(this, 'type'); 72 | 73 | store.fetchAll(type, this); 74 | }, 75 | 76 | addReference: function(reference) { 77 | get(this, 'content').addObject(reference); 78 | }, 79 | 80 | removeReference: function(reference) { 81 | get(this, 'content').removeObject(reference); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/relationships/belongs_to.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set, 2 | isNone = Ember.isNone; 3 | 4 | DS.belongsTo = function(type, options) { 5 | Ember.assert("The first argument DS.belongsTo must be a model type or string, like DS.belongsTo(App.Person)", !!type && (typeof type === 'string' || DS.Model.detect(type))); 6 | 7 | options = options || {}; 8 | 9 | var meta = { type: type, isRelationship: true, options: options, kind: 'belongsTo' }; 10 | 11 | return Ember.computed(function(key, value) { 12 | if (typeof type === 'string') { 13 | type = get(this, type, false) || get(Ember.lookup, type); 14 | } 15 | 16 | if (arguments.length === 2) { 17 | Ember.assert("You can only add a record of " + type.toString() + " to this relationship", !value || type.detectInstance(value)); 18 | return value === undefined ? null : value; 19 | } 20 | 21 | var data = get(this, 'data').belongsTo, 22 | store = get(this, 'store'), id; 23 | 24 | id = data[key]; 25 | 26 | if(isNone(id)) { 27 | return null; 28 | } else if (id.clientId) { 29 | return store.recordForReference(id); 30 | } else { 31 | return store.findById(id.type, id.id); 32 | } 33 | }).property('data').meta(meta); 34 | }; 35 | 36 | /** 37 | These observers observe all `belongsTo` relationships on the record. See 38 | `relationships/ext` to see how these observers get their dependencies. 39 | 40 | */ 41 | 42 | DS.Model.reopen({ 43 | /** @private */ 44 | belongsToWillChange: Ember.beforeObserver(function(record, key) { 45 | if (get(record, 'isLoaded')) { 46 | var oldParent = get(record, key); 47 | 48 | var childReference = get(record, '_reference'), 49 | store = get(record, 'store'); 50 | if (oldParent){ 51 | var change = DS.RelationshipChange.createChange(childReference, get(oldParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "remove" }); 52 | change.sync(); 53 | this._changesToSync[key] = change; 54 | } 55 | } 56 | }), 57 | 58 | /** @private */ 59 | belongsToDidChange: Ember.immediateObserver(function(record, key) { 60 | if (get(record, 'isLoaded')) { 61 | var newParent = get(record, key); 62 | if(newParent){ 63 | var childReference = get(record, '_reference'), 64 | store = get(record, 'store'); 65 | var change = DS.RelationshipChange.createChange(childReference, get(newParent, '_reference'), store, { key: key, kind:"belongsTo", changeType: "add" }); 66 | change.sync(); 67 | if(this._changesToSync[key]){ 68 | DS.OneToManyChange.ensureSameTransaction([change, this._changesToSync[key]], store); 69 | } 70 | } 71 | } 72 | delete this._changesToSync[key]; 73 | }) 74 | }); 75 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/transaction_adapter_test.js: -------------------------------------------------------------------------------- 1 | var transaction, adapter, store, Post, Comment; 2 | 3 | module("DS.Transaction and DS.Adapter Integration", { 4 | setup: function() { 5 | adapter = DS.Adapter.create(); 6 | store = DS.Store.create({ 7 | adapter: adapter 8 | }); 9 | 10 | transaction = store.transaction(); 11 | 12 | Post = DS.Model.extend(); 13 | Comment = DS.Model.extend({ 14 | body: DS.attr('string'), 15 | post: DS.belongsTo(Post) 16 | }); 17 | 18 | Post.reopen({ 19 | comments: DS.hasMany(Comment) 20 | }); 21 | }, 22 | 23 | teardown: function() { 24 | adapter.destroy(); 25 | store.destroy(); 26 | transaction.destroy(); 27 | Post = null; 28 | Comment = null; 29 | } 30 | }); 31 | 32 | //test("adding a clean record to a relationship causes it to be passed as an updated record", function() { 33 | //var post, comment; 34 | 35 | //adapter.commit = async(function(store, records, relationships) { 36 | //var relationship = { 37 | //child: comment, 38 | //oldParent: post1, 39 | //newParent: post2 40 | //}; 41 | 42 | //ok(records.updated.indexOf(comment) >= 0, "The comment is in the updated list"); 43 | //ok(records.updated.indexOf(post1) >= 0, "The old post is in the updated list"); 44 | //ok(records.updated.indexOf(post2) >= 0, "The new post is in the updated list"); 45 | 46 | //deepEqual(relationships.byChild.get(comment), [ relationship ]); 47 | //deepEqual(relationships.byOldParent.get(post1), [ relationship ]); 48 | //deepEqual(relationships.byNewParent.get(post2), [ relationship ]); 49 | 50 | //raises(function() { 51 | //comment.set('body', "NOPE! CHUCK TESTA!"); 52 | //}); 53 | 54 | //setTimeout(async(function() { 55 | //store.didUpdateRecord(comment); 56 | //store.didUpdateRecord(post1); 57 | //store.didUpdateRecord(post2); 58 | 59 | //var defaultTransaction = store.get('defaultTransaction'); 60 | 61 | //equal(comment.get('transaction'), defaultTransaction); 62 | //equal(post1.get('transaction'), defaultTransaction); 63 | //equal(post2.get('transaction'), defaultTransaction); 64 | //}), 1); 65 | //}); 66 | 67 | //store.load(Comment, { id: 1 }); 68 | //store.load(Post, { id: 1, comments: [ 1 ] }); 69 | //store.load(Post, { id: 2 }); 70 | 71 | //comment = store.find(Comment, 1); 72 | //var post1 = store.find(Post, 1); 73 | //var post2 = store.find(Post, 2); 74 | 75 | //transaction.add(comment); 76 | //transaction.add(post1); 77 | //transaction.add(post2); 78 | 79 | //post1.get('comments').removeObject(comment); 80 | //post2.get('comments').addObject(comment); 81 | 82 | //transaction.commit(); 83 | 84 | //equal(comment.get('transaction'), transaction); 85 | //}); 86 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/method_aliases_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | DS.Model classes contain aliases for common methods that act on the default 3 | store. This file tests that those are operating correctly. 4 | */ 5 | 6 | var get = Ember.get; 7 | var store, Person, findCalled; 8 | 9 | module("DS.Model Class Method Aliases", { 10 | setup: function() { 11 | store = DS.Store.create({ 12 | isDefaultStore: true 13 | }); 14 | 15 | Person = DS.Model.extend({ 16 | name: DS.attr('string') 17 | }); 18 | }, 19 | 20 | teardown: function() { 21 | store.destroy(); 22 | } 23 | }); 24 | 25 | test("the find method should be aliased", function() { 26 | expect(2); 27 | 28 | store.find = function(type, id) { 29 | equal(type, Person, "find called with correct type"); 30 | equal(id, 1, "find called with correct arguments"); 31 | }; 32 | 33 | Person.find(1); 34 | }); 35 | 36 | test("the filter method should be aliased", function() { 37 | expect(2); 38 | 39 | var filter = function() {}; 40 | 41 | store.filter = function(type, passedFilter) { 42 | equal(type, Person, "filter called with correct type"); 43 | equal(passedFilter, filter, "filter was called with correct arguments"); 44 | }; 45 | 46 | Person.filter(filter); 47 | }); 48 | 49 | test("the all method should be aliased", function() { 50 | expect(1); 51 | 52 | var all = function() {}; 53 | 54 | store.all = function(type) { 55 | equal(type, Person, "filter called with correct type"); 56 | }; 57 | 58 | Person.all(); 59 | }); 60 | 61 | test("the recordIsLoaded method should be aliased", function() { 62 | expect(2); 63 | 64 | var id = 1; 65 | 66 | store.recordIsLoaded = function(type, passedId) { 67 | equal(type, Person, "recordIsLoaded called with correct type"); 68 | equal(passedId, id, "recordIsLoaded was called with correct arguments"); 69 | }; 70 | 71 | Person.isLoaded(id); 72 | }); 73 | 74 | test("the create method should raise an exception", function() { 75 | raises(function() { 76 | Person.create(); 77 | }, Ember.Error); 78 | }); 79 | 80 | test("the createRecord method should be aliased", function() { 81 | expect(4); 82 | 83 | var hash = {}; 84 | 85 | store = DS.Store.createWithMixins({ 86 | isDefaultStore: true, 87 | 88 | createRecord: function(type, passedHash, transaction) { 89 | equal(type, Person, "createRecord called with correct type"); 90 | equal(hash, passedHash); 91 | 92 | return this._super(type, passedHash, transaction); 93 | } 94 | }); 95 | 96 | var person = Person.createRecord(hash); 97 | 98 | equal(get(person, 'store'), store, "the store was set"); 99 | equal(Person.detectInstance(person), true, "the person is an instance of Person"); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/application_test.js: -------------------------------------------------------------------------------- 1 | var app, container; 2 | 3 | /** 4 | These tests ensure that Ember Data works with Ember.js' application 5 | initialization and dependency injection APIs. 6 | */ 7 | 8 | if (Ember.Application.initializer) { 9 | 10 | module("Ember.Application Extensions", { 11 | setup: function() { 12 | Ember.run(function() { 13 | app = Ember.Application.create({ 14 | router: false, 15 | Store: DS.Store, 16 | FooController: Ember.Controller.extend(), 17 | ApplicationView: Ember.View.extend(), 18 | BazController: {}, 19 | ApplicationController: Ember.View.extend() 20 | }); 21 | }); 22 | 23 | container = app.__container__; 24 | }, 25 | 26 | teardown: function() { 27 | app.destroy(); 28 | Ember.BOOTED = false; 29 | } 30 | }); 31 | 32 | test("If a Store property exists on an Ember.Application, it should be instantiated.", function() { 33 | ok(container.lookup('store:main') instanceof DS.Store, "the store was instantiated"); 34 | }); 35 | 36 | test("If a store is instantiated, it should be made available to each controller.", function() { 37 | var fooController = container.lookup('controller:foo'); 38 | ok(fooController.get('store') instanceof DS.Store, "the store was injected"); 39 | }); 40 | } 41 | 42 | if (Ember.Application.registerInjection) { 43 | /** 44 | These tests ensure that Ember Data works with Ember.js' application 45 | initialization and dependency injection APIs. 46 | */ 47 | 48 | module("Ember.Application Extensions", { 49 | setup: function() { 50 | var Router = Ember.Router.extend({ 51 | root: Ember.Route.extend() 52 | }); 53 | 54 | Ember.run(function() { 55 | app = Ember.Application.create({ 56 | Router: Router, 57 | Store: DS.Store, 58 | FooController: Ember.Controller.extend(), 59 | ApplicationView: Ember.View.extend(), 60 | BazController: {}, 61 | ApplicationController: Ember.View.extend() 62 | }); 63 | }); 64 | }, 65 | 66 | teardown: function() { 67 | app.destroy(); 68 | } 69 | }); 70 | 71 | test("If a Store property exists on an Ember.Application, it should be instantiated.", function() { 72 | Ember.run(function() { app.initialize(); }); 73 | 74 | ok(app.get('router.store') instanceof DS.Store, "the store was injected"); 75 | }); 76 | 77 | test("If a store is instantiated, it should be made available to each controller.", function() { 78 | Ember.run(function() { app.initialize(); }); 79 | 80 | ok(app.get('router.fooController.store') instanceof DS.Store, "the store was injected"); 81 | }); 82 | 83 | test("It doesn't try to inject the store into non-controllers", function() { 84 | Ember.run(function() { app.initialize(); }); 85 | 86 | equal(app.get('router.bazController.store'), undefined, "the function was not injected"); 87 | }); 88 | } 89 | 90 | if (!Ember.Application.registerInjection && !Ember.Application.initializer) { 91 | test("Should support either the old or new initialization API", function() { 92 | ok(false, "Should not get here"); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/find_all_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var Person, adapter, store, allRecords; 4 | 5 | module("Finding All Records of a Type", { 6 | setup: function() { 7 | Person = DS.Model.extend({ 8 | updatedAt: DS.attr('string'), 9 | name: DS.attr('string'), 10 | firstName: DS.attr('string'), 11 | lastName: DS.attr('string') 12 | }); 13 | 14 | adapter = DS.Adapter.create(); 15 | store = DS.Store.create({ adapter: adapter }); 16 | allRecords = null; 17 | }, 18 | 19 | teardown: function() { 20 | if (allRecords) { allRecords.destroy(); } 21 | adapter.destroy(); 22 | store.destroy(); 23 | } 24 | }); 25 | 26 | test("When all records for a type are requested, the store should call the adapter's `findAll` method.", function() { 27 | expect(5); 28 | 29 | adapter.findAll = function(store, type, since) { 30 | ok(true, "the adapter's findAll method should be invoked"); 31 | 32 | // Simulate latency to ensure correct behavior in asynchronous conditions. 33 | invokeAsync(function() { 34 | store.loadMany(type, [{ id: 1, name: "Braaaahm Dale" }]); 35 | 36 | // Only one record array per type should ever be created (identity map) 37 | strictEqual(allRecords, store.all(Person), "the same record array is returned every time all records of a type are requested"); 38 | }); 39 | }; 40 | 41 | allRecords = store.find(Person); 42 | 43 | equal(get(allRecords, 'length'), 0, "the record array's length is zero before any records are loaded"); 44 | 45 | Ember.addObserver(allRecords, 'length', function() { 46 | equal(get(allRecords, 'length'), 1, "the record array's length is 1 after a record is loaded into it"); 47 | equal(allRecords.objectAt(0).get('name'), "Braaaahm Dale", "the first item in the record array is Braaaahm Dale"); 48 | }); 49 | }); 50 | 51 | test("When all records for a type are requested, records that are already loaded should be returned immediately.", function() { 52 | expect(3); 53 | 54 | // Load a record from the server 55 | store.load(Person, { id: 1, name: "Jeremy Ashkenas" }); 56 | 57 | // Create a new, unsaved record in the store 58 | store.createRecord(Person, { name: "Alex MacCaw" }); 59 | 60 | allRecords = store.all(Person); 61 | 62 | equal(get(allRecords, 'length'), 2, "the record array's length is 2"); 63 | equal(allRecords.objectAt(0).get('name'), "Jeremy Ashkenas", "the first item in the record array is Jeremy Ashkenas"); 64 | equal(allRecords.objectAt(1).get('name'), "Alex MacCaw", "the second item in the record array is Alex MacCaw"); 65 | }); 66 | 67 | test("When all records for a type are requested, records that are created on the client should be added to the record array.", function() { 68 | expect(3); 69 | 70 | allRecords = store.all(Person); 71 | 72 | equal(get(allRecords, 'length'), 0, "precond - the record array's length is zero before any records are loaded"); 73 | 74 | store.createRecord(Person, { name: "Carsten Nielsen" }); 75 | 76 | equal(get(allRecords, 'length'), 1, "the record array's length is 1"); 77 | equal(allRecords.objectAt(0).get('name'), "Carsten Nielsen", "the first item in the record array is Carsten Nielsen"); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/application_ext.js: -------------------------------------------------------------------------------- 1 | var set = Ember.set; 2 | 3 | /** 4 | This code registers an injection for Ember.Application. 5 | 6 | If an Ember.js developer defines a subclass of DS.Store on their application, 7 | this code will automatically instantiate it and make it available on the 8 | router. 9 | 10 | Additionally, after an application's controllers have been injected, they will 11 | each have the store made available to them. 12 | 13 | For example, imagine an Ember.js application with the following classes: 14 | 15 | App.Store = DS.Store.extend({ 16 | adapter: 'App.MyCustomAdapter' 17 | }); 18 | 19 | App.PostsController = Ember.ArrayController.extend({ 20 | // ... 21 | }); 22 | 23 | When the application is initialized, `App.Store` will automatically be 24 | instantiated, and the instance of `App.PostsController` will have its `store` 25 | property set to that instance. 26 | 27 | Note that this code will only be run if the `ember-application` package is 28 | loaded. If Ember Data is being used in an environment other than a 29 | typical application (e.g., node.js where only `ember-runtime` is available), 30 | this code will be ignored. 31 | */ 32 | 33 | Ember.onLoad('Ember.Application', function(Application) { 34 | if (Application.registerInjection) { 35 | Application.registerInjection({ 36 | name: "store", 37 | before: "controllers", 38 | 39 | // If a store subclass is defined, like App.Store, 40 | // instantiate it and inject it into the router. 41 | injection: function(app, stateManager, property) { 42 | if (!stateManager) { return; } 43 | if (property === 'Store') { 44 | set(stateManager, 'store', app[property].create()); 45 | } 46 | } 47 | }); 48 | 49 | Application.registerInjection({ 50 | name: "giveStoreToControllers", 51 | after: ['store','controllers'], 52 | 53 | // For each controller, set its `store` property 54 | // to the DS.Store instance we created above. 55 | injection: function(app, stateManager, property) { 56 | if (!stateManager) { return; } 57 | if (/^[A-Z].*Controller$/.test(property)) { 58 | var controllerName = property.charAt(0).toLowerCase() + property.substr(1); 59 | var store = stateManager.get('store'); 60 | var controller = stateManager.get(controllerName); 61 | if(!controller) { return; } 62 | 63 | controller.set('store', store); 64 | } 65 | } 66 | }); 67 | } else if (Application.initializer) { 68 | Application.initializer({ 69 | name: "store", 70 | 71 | initialize: function(container, application) { 72 | application.register('store:main', application.Store); 73 | 74 | // Eagerly generate the store so defaultStore is populated. 75 | // TODO: Do this in a finisher hook 76 | container.lookup('store:main'); 77 | } 78 | }); 79 | 80 | Application.initializer({ 81 | name: "injectStore", 82 | 83 | initialize: function(container, application) { 84 | application.inject('controller', 'store', 'store:main'); 85 | application.inject('route', 'store', 'store:main'); 86 | } 87 | }); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /packages/ember-data/lib/serializers/fixture_serializer.js: -------------------------------------------------------------------------------- 1 | require('ember-data/system/serializer'); 2 | 3 | /** 4 | @module data 5 | @submodule data-serializers 6 | */ 7 | 8 | /** 9 | @class FixtureSerializer 10 | @constructor 11 | @namespace DS 12 | @extends DS.Serializer 13 | */ 14 | 15 | var get = Ember.get, set = Ember.set; 16 | 17 | DS.FixtureSerializer = DS.Serializer.extend({ 18 | deserializeValue: function(value, attributeType) { 19 | return value; 20 | }, 21 | 22 | serializeValue: function(value, attributeType) { 23 | return value; 24 | }, 25 | 26 | addId: function(data, key, id) { 27 | data[key] = id; 28 | }, 29 | 30 | addAttribute: function(hash, key, value) { 31 | hash[key] = value; 32 | }, 33 | 34 | addBelongsTo: function(hash, record, key, relationship) { 35 | var id = get(record, relationship.key+'.id'); 36 | if (!Ember.isNone(id)) { hash[key] = id; } 37 | }, 38 | 39 | addHasMany: function(hash, record, key, relationship) { 40 | var ids = get(record, relationship.key).map(function(item) { 41 | return item.get('id'); 42 | }); 43 | 44 | hash[relationship.key] = ids; 45 | }, 46 | 47 | extract: function(loader, fixture, type, record) { 48 | if (record) { loader.updateId(record, fixture); } 49 | this.extractRecordRepresentation(loader, type, fixture); 50 | }, 51 | 52 | extractMany: function(loader, fixtures, type, records) { 53 | var objects = fixtures, references = []; 54 | if (records) { records = records.toArray(); } 55 | 56 | for (var i = 0; i < objects.length; i++) { 57 | if (records) { loader.updateId(records[i], objects[i]); } 58 | var reference = this.extractRecordRepresentation(loader, type, objects[i]); 59 | references.push(reference); 60 | } 61 | 62 | loader.populateArray(references); 63 | }, 64 | 65 | extractId: function(type, hash) { 66 | var primaryKey = this._primaryKey(type); 67 | 68 | if (hash.hasOwnProperty(primaryKey)) { 69 | // Ensure that we coerce IDs to strings so that record 70 | // IDs remain consistent between application runs; especially 71 | // if the ID is serialized and later deserialized from the URL, 72 | // when type information will have been lost. 73 | return hash[primaryKey]+''; 74 | } else { 75 | return null; 76 | } 77 | }, 78 | 79 | extractAttribute: function(type, hash, attributeName) { 80 | var key = this._keyForAttributeName(type, attributeName); 81 | return hash[key]; 82 | }, 83 | 84 | extractHasMany: function(type, hash, key) { 85 | return hash[key]; 86 | }, 87 | 88 | extractBelongsTo: function(type, hash, key) { 89 | return hash[key]; 90 | }, 91 | 92 | extractBelongsToPolymorphic: function(type, hash, key) { 93 | var keyForId = this.keyForPolymorphicId(key), 94 | keyForType, 95 | id = hash[keyForId]; 96 | 97 | if (id) { 98 | keyForType = this.keyForPolymorphicType(key); 99 | return {id: id, type: hash[keyForType]}; 100 | } 101 | 102 | return null; 103 | }, 104 | 105 | keyForPolymorphicId: function(key) { 106 | return key; 107 | }, 108 | 109 | keyForPolymorphicType: function(key) { 110 | return key + '_type'; 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/model/attributes.js: -------------------------------------------------------------------------------- 1 | require("ember-data/system/model/model"); 2 | 3 | /** 4 | @module data 5 | @submodule data-model 6 | */ 7 | 8 | var get = Ember.get; 9 | 10 | DS.Model.reopenClass({ 11 | attributes: Ember.computed(function() { 12 | var map = Ember.Map.create(); 13 | 14 | this.eachComputedProperty(function(name, meta) { 15 | if (meta.isAttribute) { 16 | Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), name !== 'id'); 17 | 18 | meta.name = name; 19 | map.set(name, meta); 20 | } 21 | }); 22 | 23 | return map; 24 | }) 25 | }); 26 | 27 | var AttributeChange = DS.AttributeChange = function(options) { 28 | this.reference = options.reference; 29 | this.store = options.store; 30 | this.name = options.name; 31 | this.oldValue = options.oldValue; 32 | }; 33 | 34 | AttributeChange.createChange = function(options) { 35 | return new AttributeChange(options); 36 | }; 37 | 38 | AttributeChange.prototype = { 39 | sync: function() { 40 | this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); 41 | 42 | // TODO: Use this object in the commit process 43 | this.destroy(); 44 | }, 45 | 46 | destroy: function() { 47 | delete this.store.recordForReference(this.reference)._changesToSync[this.name]; 48 | } 49 | }; 50 | 51 | DS.Model.reopen({ 52 | eachAttribute: function(callback, binding) { 53 | get(this.constructor, 'attributes').forEach(function(name, meta) { 54 | callback.call(binding, name, meta); 55 | }, binding); 56 | }, 57 | 58 | attributeWillChange: Ember.beforeObserver(function(record, key) { 59 | var reference = get(record, '_reference'), 60 | store = get(record, 'store'); 61 | 62 | record.send('willSetProperty', { reference: reference, store: store, name: key }); 63 | }), 64 | 65 | attributeDidChange: Ember.observer(function(record, key) { 66 | record.send('didSetProperty', { name: key }); 67 | }) 68 | }); 69 | 70 | function getAttr(record, options, key) { 71 | var attributes = get(record, 'data').attributes; 72 | var value = attributes[key]; 73 | 74 | if (value === undefined) { 75 | if (typeof options.defaultValue === "function") { 76 | value = options.defaultValue(); 77 | } else { 78 | value = options.defaultValue; 79 | } 80 | } 81 | 82 | return value; 83 | } 84 | 85 | DS.attr = function(type, options) { 86 | options = options || {}; 87 | 88 | var meta = { 89 | type: type, 90 | isAttribute: true, 91 | options: options 92 | }; 93 | 94 | return Ember.computed(function(key, value, oldValue) { 95 | if (arguments.length > 1) { 96 | Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); 97 | } else { 98 | value = getAttr(this, options, key); 99 | } 100 | 101 | return value; 102 | // `data` is never set directly. However, it may be 103 | // invalidated from the state manager's setData 104 | // event. 105 | }).property('data').meta(meta); 106 | }; 107 | 108 | -------------------------------------------------------------------------------- /packages/ember-data/lib/transforms/json_transforms.js: -------------------------------------------------------------------------------- 1 | var none = Ember.isNone, empty = Ember.isEmpty; 2 | 3 | /** 4 | @module data 5 | @submodule data-transforms 6 | */ 7 | 8 | /** 9 | DS.Transforms is a hash of transforms used by DS.Serializer. 10 | 11 | @class JSONTransforms 12 | @static 13 | @namespace DS 14 | */ 15 | DS.JSONTransforms = { 16 | string: { 17 | deserialize: function(serialized) { 18 | return none(serialized) ? null : String(serialized); 19 | }, 20 | 21 | serialize: function(deserialized) { 22 | return none(deserialized) ? null : String(deserialized); 23 | } 24 | }, 25 | 26 | number: { 27 | deserialize: function(serialized) { 28 | return empty(serialized) ? null : Number(serialized); 29 | }, 30 | 31 | serialize: function(deserialized) { 32 | return empty(deserialized) ? null : Number(deserialized); 33 | } 34 | }, 35 | 36 | // Handles the following boolean inputs: 37 | // "TrUe", "t", "f", "FALSE", 0, (non-zero), or boolean true/false 38 | 'boolean': { 39 | deserialize: function(serialized) { 40 | var type = typeof serialized; 41 | 42 | if (type === "boolean") { 43 | return serialized; 44 | } else if (type === "string") { 45 | return serialized.match(/^true$|^t$|^1$/i) !== null; 46 | } else if (type === "number") { 47 | return serialized === 1; 48 | } else { 49 | return false; 50 | } 51 | }, 52 | 53 | serialize: function(deserialized) { 54 | return Boolean(deserialized); 55 | } 56 | }, 57 | 58 | date: { 59 | deserialize: function(serialized) { 60 | var type = typeof serialized; 61 | 62 | if (type === "string") { 63 | return new Date(Ember.Date.parse(serialized)); 64 | } else if (type === "number") { 65 | return new Date(serialized); 66 | } else if (serialized === null || serialized === undefined) { 67 | // if the value is not present in the data, 68 | // return undefined, not null. 69 | return serialized; 70 | } else { 71 | return null; 72 | } 73 | }, 74 | 75 | serialize: function(date) { 76 | if (date instanceof Date) { 77 | var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 78 | var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 79 | 80 | var pad = function(num) { 81 | return num < 10 ? "0"+num : ""+num; 82 | }; 83 | 84 | var utcYear = date.getUTCFullYear(), 85 | utcMonth = date.getUTCMonth(), 86 | utcDayOfMonth = date.getUTCDate(), 87 | utcDay = date.getUTCDay(), 88 | utcHours = date.getUTCHours(), 89 | utcMinutes = date.getUTCMinutes(), 90 | utcSeconds = date.getUTCSeconds(); 91 | 92 | 93 | var dayOfWeek = days[utcDay]; 94 | var dayOfMonth = pad(utcDayOfMonth); 95 | var month = months[utcMonth]; 96 | 97 | return dayOfWeek + ", " + dayOfMonth + " " + month + " " + utcYear + " " + 98 | pad(utcHours) + ":" + pad(utcMinutes) + ":" + pad(utcSeconds) + " GMT"; 99 | } else { 100 | return null; 101 | } 102 | } 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/adapters/basic_adapter/commit_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get; 2 | 3 | var store, adapter, Person, PhoneNumber; 4 | module("Basic Adapter - Saving", { 5 | setup: function() { 6 | adapter = DS.BasicAdapter.create(); 7 | store = DS.Store.create({ 8 | adapter: adapter 9 | }); 10 | 11 | var attr = DS.attr, hasMany = DS.hasMany, belongsTo = DS.belongsTo; 12 | Person = DS.Model.extend({ 13 | firstName: attr('string'), 14 | lastName: attr('string'), 15 | createdAt: attr('date') 16 | }); 17 | 18 | PhoneNumber = DS.Model.extend({ 19 | areaCode: attr('number'), 20 | number: attr('number'), 21 | person: belongsTo(Person) 22 | }); 23 | 24 | Person.reopen({ 25 | phoneNumbers: hasMany(PhoneNumber) 26 | }); 27 | 28 | DS.registerTransforms('test', { 29 | date: { 30 | serialize: function(value) { 31 | return value.toString(); 32 | }, 33 | 34 | deserialize: function(string) { 35 | return new Date(string); 36 | } 37 | } 38 | }); 39 | }, 40 | 41 | teardown: function() { 42 | Ember.run(function() { 43 | DS.clearTransforms(); 44 | store.destroy(); 45 | adapter.destroy(); 46 | }); 47 | } 48 | }); 49 | 50 | test("After creating a record, calling `save` on it will save it using the BasicAdapter", function() { 51 | expect(2); 52 | 53 | Person.sync = { 54 | createRecord: function(passedRecord, process) { 55 | equal(passedRecord, person, "The person was passed through"); 56 | process(passedRecord).save(function(json) { 57 | deepEqual(json, { firstName: "Igor", lastName: "Terzic", createdAt: null }, "The process method toJSON'ifies the record"); 58 | }); 59 | } 60 | }; 61 | 62 | var person = Person.createRecord({ firstName: "Igor", lastName: "Terzic" }); 63 | person.save(); 64 | }); 65 | 66 | test("After updating a record, calling `save` on it will save it using the BasicAdapter", function() { 67 | expect(2); 68 | 69 | Person.sync = { 70 | updateRecord: function(passedRecord, process) { 71 | equal(passedRecord, person, "The person was passed through"); 72 | process(passedRecord).save(function(json) { 73 | deepEqual(json, { id: 1, firstName: "Igor", lastName: "Terzicsta", createdAt: null }, "The process method toJSON'ifies the record"); 74 | }); 75 | } 76 | }; 77 | 78 | store.load(Person, { id: 1, firstName: "Igor", lastName: "Terzic" }); 79 | var person = Person.find(1); 80 | person.set('lastName', "Terzicsta"); 81 | 82 | person.save(); 83 | }); 84 | 85 | test("After deleting a record, calling `save` on it will save it using the BasicAdapter", function() { 86 | expect(2); 87 | 88 | Person.sync = { 89 | deleteRecord: function(passedRecord, process) { 90 | equal(passedRecord, person, "The person was passed through"); 91 | process(passedRecord).save(function(json) { 92 | deepEqual(json, { id: 1, firstName: "Igor", lastName: "Terzic", createdAt: null }, "The process method toJSON'ifies the record"); 93 | }); 94 | } 95 | }; 96 | 97 | store.load(Person, { id: 1, firstName: "Igor", lastName: "Terzic" }); 98 | var person = Person.find(1); 99 | person.deleteRecord(); 100 | person.save(); 101 | }); 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ember Data [![Build Status](https://secure.travis-ci.org/emberjs/data.png?branch=master)](http://travis-ci.org/emberjs/data) 2 | 3 | Ember Data is a library for loading data from a persistence layer (such as 4 | a JSON API), mapping this data to a set of models within your client application, 5 | updating those models, then saving the changes back to a persistence layer. It 6 | provides many of the facilities you'd find in server-side ORMs like ActiveRecord, but is 7 | designed specifically for the unique environment of JavaScript in the browser. 8 | 9 | Ember Data provides a central Data Store, which can be configured with a range of 10 | provided Adapters, but two core Adapters are provided: the RESTAdapter and BasicAdapter. 11 | 12 | The RESTAdapter is configured for use by default. You can read more about it in 13 | the [Guides](http://emberjs.com/guides/models/the-rest-adapter/). It provides a fully 14 | RESTful mechanism for communicating with your persistence layer, and is the preferred 15 | and recommened choice for use with Ember Data. 16 | 17 | The BasicAdapter is intended to provide a way for developers who want full control 18 | over how the persistence layer is communicated with via their own implemented Ajax 19 | hooks 20 | 21 | This is definitely alpha-quality. The basics of RESTAdapter work, but there are for 22 | sure edge cases that are not yet handled. Please report any bugs or feature 23 | requests, and pull requests are always welcome. The BasicAdapter is under heavy 24 | development at present. 25 | 26 | #### Is It Good? 27 | 28 | Yes. 29 | 30 | #### Is It "Production Ready™"? 31 | 32 | No. The API should not be considered stable until 1.0. Breaking changes, 33 | indexed by date, are listed in [`BREAKING_CHANGES.md`](https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md). 34 | 35 | A [guide is provided on the Ember.js site](http://emberjs.com/guides/models/) that is accurate as of revision 11. 36 | 37 | #### Getting ember-data 38 | 39 | Currently you must build ember-data.js yourself. Clone the repository, run `bundle` then `rake dist`. You'll find ember-data.js in the `dist` directory. 40 | 41 | #### Roadmap 42 | 43 | * Handle error states 44 | * Better built-in attributes 45 | * Editing "forked" records 46 | * Out-of-the-box support for Rails apps that follow the 47 | [`active_model_serializers`](https://github.com/rails-api/active_model_serializers) gem's conventions. 48 | * Handle partially-loaded records 49 | 50 | ## How to Run Unit Tests 51 | 52 | ### Setup 53 | 54 | 1. Install Ruby 1.9.2+. There are many resources on the web can help; one of the best is [rvm](https://rvm.io/). 55 | 56 | 2. Install Bundler: `gem install bundler` 57 | 58 | 3. Run `bundle` inside the project root to install the gem dependencies. 59 | 60 | ### In Your Browser 61 | 62 | 1. To start the development server, run `rackup`. 63 | 64 | 2. Then visit: `http://localhost:9292/?package=PACKAGE_NAME`. Replace `PACKAGE_NAME` with the name of the package you want to run. For example: 65 | 66 | * [Ember.js Data](http://localhost:9292/?package=ember-data) 67 | 68 | To run multiple packages, you can separate them with commas. You can run all the tests using the `all` package: 69 | 70 | 71 | 72 | You can also pass `jquery=VERSION` in the test URL to test different versions of jQuery. Default is 1.9.0. 73 | 74 | ### From the CLI 75 | 76 | 1. Install phantomjs from http://phantomjs.org 77 | 78 | 2. Run `rake test` to run a basic test suite or run `rake test[all]` to 79 | run a more comprehensive suite. 80 | 81 | 3. (Mac OS X Only) Run `rake autotest` to automatically re-run tests 82 | when any files are changed. 83 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/reload_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var Person, store, adapter; 3 | 4 | module("Reloading Records", { 5 | setup: function() { 6 | Person = DS.Model.extend({ 7 | updatedAt: DS.attr('string'), 8 | name: DS.attr('string'), 9 | firstName: DS.attr('string'), 10 | lastName: DS.attr('string') 11 | }); 12 | 13 | Person.toString = function() { return "Person"; }; 14 | 15 | adapter = DS.Adapter.create(); 16 | store = DS.Store.create({ adapter: adapter }); 17 | }, 18 | 19 | teardown: function() { 20 | adapter.destroy(); 21 | store.destroy(); 22 | } 23 | }); 24 | 25 | asyncTest("When a single record is requested, the adapter's find method should be called unless it's loaded.", function() { 26 | expect(5); 27 | 28 | var count = 0; 29 | 30 | adapter.find = function(store, type, id) { 31 | if (count === 0) { 32 | setTimeout(function() { 33 | adapter.didFindRecord(store, type, { person: { name: "Tom Dale" } }, id); 34 | firstFound(); 35 | }); 36 | count++; 37 | } else if (count === 1) { 38 | setTimeout(function() { 39 | adapter.didFindRecord(store, type, { person: { name: "Braaaahm Dale" } }, id); 40 | secondFound(); 41 | }); 42 | count++; 43 | } else { 44 | ok(false, "Should not get here"); 45 | } 46 | }; 47 | 48 | var person = store.find(Person, 1); 49 | 50 | var waitingFor = 2; 51 | 52 | function done() { 53 | if (--waitingFor === 0) { start(); } 54 | } 55 | 56 | function firstFound() { 57 | equal(get(person, 'name'), "Tom Dale", "The person is loaded with the right name"); 58 | equal(get(person, 'isLoaded'), true, "The person is now loaded"); 59 | person.one('didReload', done); 60 | person.reload(); 61 | equal(get(person, 'isReloading'), true, "The person is now reloading"); 62 | } 63 | 64 | function secondFound() { 65 | done(); 66 | equal(get(person, 'isReloading'), false, "The person is no longer reloading"); 67 | equal(get(person, 'name'), "Braaaahm Dale", "The person is now updated with the right name"); 68 | } 69 | }); 70 | 71 | asyncTest("If a record is modified, it cannot be reloaded", function() { 72 | var count = 0; 73 | 74 | adapter.find = function(store, type, id) { 75 | if (count === 0) { 76 | setTimeout(function() { 77 | adapter.didFindRecord(store, type, { person: { name: "Tom Dale" } }, id); 78 | found(); 79 | }); 80 | count++; 81 | } else { 82 | ok(false, "Should not get here"); 83 | } 84 | }; 85 | 86 | var person = store.find(Person, 1); 87 | 88 | function found() { 89 | start(); 90 | set(person, 'name', "Braaaaahm Dale"); 91 | 92 | raises(function() { 93 | person.reload(); 94 | }, /uncommitted/); 95 | } 96 | }); 97 | 98 | 99 | asyncTest("When a record is loaded a second time, isLoaded stays true", function() { 100 | store.load(Person, { id: 1, name: "Tom Dale" }); 101 | 102 | var person = store.find(Person, 1); 103 | 104 | equal(get(person, 'isLoaded'), true, "The person is loaded"); 105 | 106 | function isLoadedDidChange() { 107 | // This shouldn't be hit 108 | equal(get(person, 'isLoaded'), true, "The person is still loaded after change"); 109 | } 110 | person.addObserver('isLoaded', isLoadedDidChange); 111 | 112 | // Reload the record 113 | store.load(Person, { id: 1, name: "Tom Dale" }); 114 | equal(get(person, 'isLoaded'), true, "The person is still loaded after load"); 115 | 116 | person.removeObserver('isLoaded', isLoadedDidChange); 117 | 118 | start(); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /tests/ember_configuration.js: -------------------------------------------------------------------------------- 1 | /*globals EmberDev ENV QUnit */ 2 | 3 | (function() { 4 | window.Ember = window.Ember || {}; 5 | 6 | Ember.config = {}; 7 | Ember.testing = true; 8 | 9 | window.ENV = { TESTING: true }; 10 | 11 | var extendPrototypes = QUnit.urlParams.extendprototypes; 12 | ENV['EXTEND_PROTOTYPES'] = !!extendPrototypes; 13 | 14 | if (EmberDev.jsHint) { 15 | // jsHint makes its own Object.create stub, we don't want to use this 16 | ENV['STUB_OBJECT_CREATE'] = !Object.create; 17 | } 18 | 19 | window.async = function(callback, timeout) { 20 | stop(); 21 | 22 | timeout = setTimeout(function() { 23 | start(); 24 | ok(false, "Timeout was reached"); 25 | }, timeout || 100); 26 | 27 | return function() { 28 | clearTimeout(timeout); 29 | 30 | start(); 31 | 32 | var args = arguments; 33 | Ember.run(function() { 34 | callback.apply(this, args); 35 | }); 36 | }; 37 | }; 38 | 39 | window.invokeAsync = function(callback, timeout) { 40 | timeout = timeout || 1; 41 | 42 | setTimeout(async(callback, timeout+100), timeout); 43 | }; 44 | 45 | var syncForTest = function(fn) { 46 | var callSuper; 47 | 48 | if (typeof fn !== "function") { callSuper = true; } 49 | 50 | return function() { 51 | var override = false, ret; 52 | 53 | if (Ember.run && !Ember.run.currentRunLoop) { 54 | Ember.run.begin(); 55 | override = true; 56 | } 57 | 58 | try { 59 | if (callSuper) { 60 | ret = this._super.apply(this, arguments); 61 | } else { 62 | ret = fn.apply(this, arguments); 63 | } 64 | } finally { 65 | if (override) { 66 | Ember.run.end(); 67 | } 68 | } 69 | 70 | return ret; 71 | }; 72 | }; 73 | 74 | Ember.config.overrideAccessors = function() { 75 | Ember.set = syncForTest(Ember.set); 76 | Ember.get = syncForTest(Ember.get); 77 | }; 78 | 79 | Ember.config.overrideClassMixin = function(ClassMixin) { 80 | ClassMixin.reopen({ 81 | create: syncForTest() 82 | }); 83 | }; 84 | 85 | Ember.config.overridePrototypeMixin = function(PrototypeMixin) { 86 | PrototypeMixin.reopen({ 87 | destroy: syncForTest() 88 | }); 89 | }; 90 | 91 | 92 | minispade.register('ember-data/~test-setup', function() { 93 | Ember.View.reopen({ 94 | _insertElementLater: syncForTest() 95 | }); 96 | 97 | DS.Store.reopen({ 98 | createRecord: syncForTest(), 99 | deleteRecord: syncForTest(), 100 | load: syncForTest(), 101 | loadMany: syncForTest(), 102 | filter: syncForTest(), 103 | find: syncForTest(), 104 | findByClientId: syncForTest(), 105 | findMany: syncForTest(), 106 | didSaveRecord: syncForTest(), 107 | didSaveRecords: syncForTest(), 108 | didUpdateAttribute: syncForTest(), 109 | didUpdateAttributes: syncForTest(), 110 | didUpdateRelationship: syncForTest(), 111 | didUpdateRelationships: syncForTest() 112 | }); 113 | 114 | DS.Model.reopen({ 115 | then: syncForTest(), 116 | save: syncForTest(), 117 | deleteRecord: syncForTest(), 118 | dataDidChange: Ember.observer(syncForTest(), 'data'), 119 | updateRecordArraysLater: syncForTest() 120 | }); 121 | 122 | DS.RecordArray.reopen({ 123 | then: syncForTest() 124 | }); 125 | 126 | DS.Transaction.reopen({ 127 | commit: syncForTest() 128 | }); 129 | }); 130 | 131 | EmberDev.distros = { 132 | //spade: 'ember-data-spade.js', 133 | spade: 'ember-spade.js', 134 | build: 'ember-data.js' 135 | }; 136 | })(); 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Questions 2 | 3 | This is the issue tracker for Ember Data. The Ember.js community uses this site 4 | to collect and track bugs and discussions of new features. If you are having 5 | difficulties using Ember Data or have a question about usage please ask a 6 | question on StackOverflow: http://stackoverflow.com/questions/ask and tag 7 | your question with `ember.js` and `ember-data`. 8 | 9 | The Ember.js community is very active on StackOverflow and most questions 10 | receive attention the same day they're posted: 11 | http://stackoverflow.com/questions/tagged/ember.js 12 | http://stackoverflow.com/questions/tagged/ember-data 13 | 14 | # Issues 15 | 16 | Think you've found a bug or have a new feature to suggest? Let us know! 17 | 18 | ## Reporting a Bug 19 | 1. Update to the most recent master release if possible. We may have already 20 | fixed your bug. 21 | 22 | 2. Search for similar issues. It's possible somebody has encountered 23 | this bug already. 24 | 25 | 3. Provide JSFiddle or JSBin demo that specifically shows the problem. This 26 | demo should be fully operational with the exception of the bug you want to 27 | demonstrate. The more pared down, the better. 28 | 29 | 4. If possible, submit a Pull Request with a failing test. Better yet, take 30 | a stab at fixing the bug yourself if you can! 31 | 32 | The more information you provide, the easier it is for us to validate that 33 | there is a bug and the faster we'll be able to take action. 34 | 35 | ## Requesting a Feature 36 | 37 | 1. Search Issues for similar feature requests. It's possible somebody has 38 | already asked for this feature or provided a pull request that we're still 39 | discussing. 40 | 41 | 2. Provide a clear and detailed explanation of the feature you want and why 42 | it's important to add. Keep in mind that we want features that will be useful 43 | to the majority of our users and not just a small subset. If you're just 44 | targeting a minority of users, consider writing an add-on library for Ember. 45 | 46 | 3. If the feature is complex, consider writing some initial documentation for 47 | it. If we do end up accepting the feature it will need to be documented and 48 | this will also help us to understand it better ourselves. 49 | 50 | 4. Attempt a Pull Request. If you're at all able, start writing some code. We 51 | always have more work to do than time to do it. If you can write some code 52 | then that will speed the process along. 53 | 54 | # Pull Requests 55 | 56 | We love pull requests. Here's a quick guide: 57 | 58 | 1. Fork the repo. 59 | 60 | 2. Run the tests. We only take pull requests with passing tests, and it's great 61 | to know that you have a clean slate: `bundle && rake test[all]`. (To see tests 62 | in the browser, run `rackup` and open `http://localhost:9292/?package=all`.) 63 | 64 | 3. Add a test for your change. Only refactoring and documentation changes 65 | require no new tests. If you are adding functionality or fixing a bug, we need 66 | a test! 67 | 68 | 4. Make the test pass. 69 | 70 | 5. Push to your fork and submit a pull request. Please provide us with some 71 | explanation of why you made the changes you made. For new features make sure to 72 | explain a standard use case to us. 73 | 74 | We try to be quick about responding to tickets but sometimes we get a bit 75 | backlogged. If the response is slow, try to find someone on IRC (#emberjs) to 76 | give the ticket a review. 77 | 78 | Some things that will increase the chance that your pull request is accepted, 79 | taken straight from the Ruby on Rails guide: 80 | 81 | * Use Ember idioms and helpers 82 | * Include tests that fail without your code, and pass with it 83 | * Update the documentation, the surrounding one, examples elsewhere, guides, 84 | whatever is affected by your contribution 85 | 86 | Syntax: 87 | 88 | * Two spaces, no tabs. 89 | * No trailing whitespace. Blank lines should not have any space. 90 | * a = b and not a=b. 91 | * Follow the conventions you see used in the source already. 92 | 93 | And in case we didn't emphasize it enough: we love tests! 94 | 95 | NOTE: Partially copied from https://raw.github.com/thoughtbot/factory_girl_rails/master/CONTRIBUTING.md 96 | -------------------------------------------------------------------------------- /packages/ember-data/tests/unit/serializers/rest_serializer_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var serializer; 4 | 5 | module("DS.RESTSerializer", { 6 | setup: function() { 7 | serializer = DS.RESTSerializer.create(); 8 | serializer.configure('plurals', { 9 | person: 'people' 10 | }); 11 | }, 12 | teardown: function() { 13 | serializer.destroy(); 14 | } 15 | }); 16 | 17 | test("keyForAttributeName returns decamelized property name", function() { 18 | equal(serializer.keyForAttributeName(DS.Model, 'myName'), 'my_name'); 19 | equal(serializer.keyForAttributeName(DS.Model, 'my_name'), 'my_name'); 20 | }); 21 | 22 | test("keyForBelongsTo returns the key appended with '_id'", function() { 23 | equal(serializer.keyForBelongsTo(DS.Model, 'person'), 'person_id'); 24 | equal(serializer.keyForBelongsTo(DS.Model, 'town'), 'town_id'); 25 | equal(serializer.keyForBelongsTo(DS.Model, 'homeTown'), 'home_town_id'); 26 | }); 27 | 28 | test("keyForHasMany returns the singularized key appended with '_ids'", function() { 29 | equal(serializer.keyForHasMany(DS.Model, 'people'), 'person_ids'); 30 | equal(serializer.keyForHasMany(DS.Model, 'towns'), 'town_ids'); 31 | equal(serializer.keyForHasMany(DS.Model, 'homeTowns'), 'home_town_ids'); 32 | }); 33 | 34 | test("extractBelongsToPolymorphic returns a tuple containing the type", function() { 35 | deepEqual(serializer.extractBelongsToPolymorphic(DS.Model, {message_id: 2, message_type: 'post'}, 'message_id'), {id: 2, type: 'post'}); 36 | }); 37 | 38 | test("Calling extract on a JSON payload with multiple records will tear them apart and call loader", function() { 39 | var App = Ember.Namespace.create({ 40 | toString: function() { return "App"; } 41 | }); 42 | 43 | App.Group = DS.Model.extend({ 44 | name: DS.attr('string') 45 | }); 46 | 47 | App.Post = DS.Model.extend({ 48 | title: DS.attr('string'), 49 | groups: DS.hasMany(App.Group) 50 | }); 51 | 52 | serializer.configure(App.Group, { 53 | sideloadAs: 'groups' 54 | }); 55 | 56 | var payload = { 57 | post: { 58 | id: 1, 59 | title: "Fifty Ways to Bereave Your Lover", 60 | groups: [1] 61 | }, 62 | 63 | groups: [{ id: 1, name: "Trolls" }] 64 | }; 65 | 66 | var loadCallCount = 0, 67 | loadMainCallCount = 0; 68 | 69 | var loader = { 70 | sideload: function(type, data, prematerialized) { 71 | loadCallCount++; 72 | }, 73 | 74 | load: function(type, data, prematerialized) { 75 | loadMainCallCount++; 76 | }, 77 | 78 | prematerialize: Ember.K 79 | }; 80 | 81 | serializer.extract(loader, payload, App.Post); 82 | 83 | equal(loadMainCallCount, 1, "one main record was loaded from a single payload"); 84 | equal(loadCallCount, 1, "one secondary record was loaded from a single payload"); 85 | 86 | //this.extractRecord(type, structure, loader) 87 | 88 | //function extractRecord(type, structure, loader) { 89 | //loader.load(type, structure, { 90 | //id: this.extractId(structure), 91 | //hasMany: { comments: [ 1,2,3 ] } 92 | //}); 93 | //} 94 | }); 95 | 96 | test("Sideloading can be done by specifying only an alias", function() { 97 | var App = Ember.Namespace.create({ 98 | toString: function() { return "App"; } 99 | }); 100 | 101 | App.Group = DS.Model.extend({ 102 | name: DS.attr('string') 103 | }); 104 | 105 | App.Post = DS.Model.extend({ 106 | title: DS.attr('string') 107 | }); 108 | 109 | serializer.configure(App.Group, { 110 | alias: 'group' 111 | }); 112 | 113 | var payload = { 114 | post: { 115 | id: 1, 116 | title: "Fifty Ways to Bereave Your Lover" 117 | }, 118 | 119 | groups: [{ id: 1, name: "Trolls" }] 120 | }; 121 | 122 | var loadCallCount = 0, 123 | loadMainCallCount = 0; 124 | 125 | var loader = { 126 | sideload: function(type, data, prematerialized) { 127 | if(type === App.Group) { 128 | loadCallCount++; 129 | } 130 | }, 131 | 132 | load: function(type, data, prematerialized) { 133 | loadMainCallCount++; 134 | }, 135 | 136 | prematerialize: Ember.K 137 | }; 138 | 139 | serializer.extract(loader, payload, App.Post); 140 | 141 | equal(loadMainCallCount, 1, "one main record was loaded from a single payload"); 142 | equal(loadCallCount, 1, "one secondary record was loaded from a single payload"); 143 | }); 144 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/mixins/mappable.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get; 2 | 3 | /** 4 | The Mappable mixin is designed for classes that would like to 5 | behave as a map for configuration purposes. 6 | 7 | For example, the DS.Adapter class can behave like a map, with 8 | more semantic API, via the `map` API: 9 | 10 | DS.Adapter.map('App.Person', { firstName: { key: 'FIRST' } }); 11 | 12 | Class configuration via a map-like API has a few common requirements 13 | that differentiate it from the standard Ember.Map implementation. 14 | 15 | First, values often are provided as strings that should be normalized 16 | into classes the first time the configuration options are used. 17 | 18 | Second, the values configured on parent classes should also be taken 19 | into account. 20 | 21 | Finally, setting the value of a key sometimes should merge with the 22 | previous value, rather than replacing it. 23 | 24 | This mixin provides a instance method, `createInstanceMapFor`, that 25 | will reify all of the configuration options set on an instance's 26 | constructor and provide it for the instance to use. 27 | 28 | Classes can implement certain hooks that allow them to customize 29 | the requirements listed above: 30 | 31 | * `resolveMapConflict` - called when a value is set for an existing 32 | value 33 | * `transformMapKey` - allows a key name (for example, a global path 34 | to a class) to be normalized 35 | * `transformMapValue` - allows a value (for example, a class that 36 | should be instantiated) to be normalized 37 | 38 | Classes that implement this mixin should also implement a class 39 | method built using the `generateMapFunctionFor` method: 40 | 41 | DS.Adapter.reopenClass({ 42 | map: DS.Mappable.generateMapFunctionFor('attributes', function(key, newValue, map) { 43 | var existingValue = map.get(key); 44 | 45 | for (var prop in newValue) { 46 | if (!newValue.hasOwnProperty(prop)) { continue; } 47 | existingValue[prop] = newValue[prop]; 48 | } 49 | }) 50 | }); 51 | 52 | The function passed to `generateMapFunctionFor` is invoked every time a 53 | new value is added to the map. 54 | 55 | @class _Mappable 56 | @private 57 | @namespace DS 58 | @extends Ember.Mixin 59 | **/ 60 | 61 | var resolveMapConflict = function(oldValue, newValue) { 62 | return oldValue; 63 | }; 64 | 65 | var transformMapKey = function(key, value) { 66 | return key; 67 | }; 68 | 69 | var transformMapValue = function(key, value) { 70 | return value; 71 | }; 72 | 73 | DS._Mappable = Ember.Mixin.create({ 74 | createInstanceMapFor: function(mapName) { 75 | var instanceMeta = Ember.metaPath(this, ['DS.Mappable'], true); 76 | 77 | instanceMeta.values = instanceMeta.values || {}; 78 | 79 | if (instanceMeta.values[mapName]) { return instanceMeta.values[mapName]; } 80 | 81 | var instanceMap = instanceMeta.values[mapName] = new Ember.Map(); 82 | 83 | var klass = this.constructor; 84 | 85 | while (klass && klass !== DS.Store) { 86 | this._copyMap(mapName, klass, instanceMap); 87 | klass = klass.superclass; 88 | } 89 | 90 | instanceMeta.values[mapName] = instanceMap; 91 | return instanceMap; 92 | }, 93 | 94 | _copyMap: function(mapName, klass, instanceMap) { 95 | var classMeta = Ember.metaPath(klass, ['DS.Mappable'], true); 96 | 97 | var classMap = classMeta[mapName]; 98 | if (classMap) { 99 | classMap.forEach(eachMap, this); 100 | } 101 | 102 | function eachMap(key, value) { 103 | var transformedKey = (klass.transformMapKey || transformMapKey)(key, value); 104 | var transformedValue = (klass.transformMapValue || transformMapValue)(key, value); 105 | 106 | var oldValue = instanceMap.get(transformedKey); 107 | var newValue = transformedValue; 108 | 109 | if (oldValue) { 110 | newValue = (this.constructor.resolveMapConflict || resolveMapConflict)(oldValue, newValue); 111 | } 112 | 113 | instanceMap.set(transformedKey, newValue); 114 | } 115 | } 116 | 117 | 118 | }); 119 | 120 | DS._Mappable.generateMapFunctionFor = function(mapName, transform) { 121 | return function(key, value) { 122 | var meta = Ember.metaPath(this, ['DS.Mappable'], true); 123 | var map = meta[mapName] || Ember.MapWithDefault.create({ 124 | defaultValue: function() { return {}; } 125 | }); 126 | 127 | transform.call(this, key, value, map); 128 | 129 | meta[mapName] = map; 130 | }; 131 | }; 132 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/per_type_adapters.js: -------------------------------------------------------------------------------- 1 | var oldLookup, lookup; 2 | 3 | module("Per-Type Adapters", { 4 | setup: function() { 5 | oldLookup = Ember.lookup; 6 | lookup = Ember.lookup = {}; 7 | }, 8 | 9 | teardown: function() { 10 | Ember.lookup = oldLookup; 11 | } 12 | }); 13 | 14 | test("Adapters can be registered on a per-type basis", function() { 15 | expect(2); 16 | 17 | var Store = DS.Store.extend(), 18 | Post = DS.Model.extend(), 19 | Comment = DS.Model.extend(); 20 | 21 | Store.registerAdapter(Post, DS.Adapter.extend({ 22 | find: function(store, type, id) { 23 | strictEqual(type, Post, "Post adapter was used to find Post record"); 24 | } 25 | })); 26 | 27 | var store = Store.create({ 28 | adapter: DS.Adapter.extend({ 29 | find: function(store, type, id) { 30 | strictEqual(type, Comment, "default adapter is used to find Comment"); 31 | } 32 | }) 33 | }); 34 | 35 | store.find(Post, 1); 36 | store.find(Comment, 1); 37 | }); 38 | 39 | test("Mapped adapters are inherited from their parents", function() { 40 | expect(2); 41 | 42 | var ParentStore = DS.Store.extend(), 43 | ChildStore = ParentStore.extend(), 44 | Post = DS.Model.extend(), 45 | Comment = DS.Model.extend(); 46 | 47 | ParentStore.registerAdapter(Post, DS.Adapter.extend({ 48 | find: function(store, type, id) { 49 | strictEqual(type, Post, "Post adapter was used to find Post record"); 50 | } 51 | })); 52 | 53 | ChildStore.registerAdapter(Comment, DS.Adapter.extend({ 54 | find: function(store, type, id) { 55 | strictEqual(type, Comment, "Comment adapter is used to find Comment"); 56 | } 57 | })); 58 | 59 | var store = ChildStore.create(); 60 | 61 | store.find(Post, 1); 62 | store.find(Comment, 1); 63 | }); 64 | 65 | test("Types can be specified as strings", function() { 66 | expect(1); 67 | 68 | var Store = DS.Store.extend(), 69 | Post = DS.Model.extend(); 70 | 71 | lookup.Post = Post; 72 | 73 | Store.registerAdapter('Post', DS.Adapter.extend({ 74 | find: function(store, type, id) { 75 | strictEqual(type, Post, "Post adapter was used to find Post record"); 76 | } 77 | })); 78 | 79 | var store = Store.create(); 80 | store.find(Post, 1); 81 | }); 82 | 83 | test("Child classes can override the mappings of parent classes", function() { 84 | expect(1); 85 | 86 | var ParentStore = DS.Store.extend(), 87 | ChildStore = ParentStore.extend(), 88 | Post = DS.Model.extend(); 89 | 90 | ParentStore.registerAdapter(Post, DS.Adapter.extend({ 91 | find: function(store, type, id) { 92 | ok(false, "parent adapter mapping should not have been reached"); 93 | } 94 | })); 95 | 96 | ChildStore.registerAdapter(Post, DS.Adapter.extend({ 97 | find: function(store, type, id) { 98 | strictEqual(type, Post, "Child adapter is used to find Post"); 99 | } 100 | })); 101 | 102 | var store = ChildStore.create(); 103 | 104 | store.find(Post, 1); 105 | }); 106 | 107 | test("Child classes can override the mappings of parent classes when types are provided as strings", function() { 108 | expect(1); 109 | 110 | var ParentStore = DS.Store.extend(), 111 | ChildStore = ParentStore.extend(), 112 | Post = DS.Model.extend(); 113 | 114 | lookup.Post = Post; 115 | 116 | ParentStore.registerAdapter('Post', DS.Adapter.extend({ 117 | find: function(store, type, id) { 118 | ok(false, "parent adapter mapping should not have been reached"); 119 | } 120 | })); 121 | 122 | ChildStore.registerAdapter(Post, DS.Adapter.extend({ 123 | find: function(store, type, id) { 124 | strictEqual(type, Post, "Child adapter is used to find Post"); 125 | } 126 | })); 127 | 128 | var store = ChildStore.create(); 129 | 130 | store.find(Post, 1); 131 | }); 132 | 133 | test("Child classes can override the mappings of parent classes when types are provided as strings", function() { 134 | expect(1); 135 | 136 | var ParentStore = DS.Store.extend(), 137 | ChildStore = ParentStore.extend(), 138 | Post = DS.Model.extend(); 139 | 140 | lookup.Post = Post; 141 | 142 | ParentStore.registerAdapter(Post, DS.Adapter.extend({ 143 | find: function(store, type, id) { 144 | ok(false, "parent adapter mapping should not have been reached"); 145 | } 146 | })); 147 | 148 | ChildStore.registerAdapter('Post', DS.Adapter.extend({ 149 | find: function(store, type, id) { 150 | strictEqual(type, Post, "Child adapter is used to find Post"); 151 | } 152 | })); 153 | 154 | var store = ChildStore.create(); 155 | 156 | store.find(Post, 1); 157 | }); 158 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/rest_adapter_test.js: -------------------------------------------------------------------------------- 1 | var store, adapter, Post, Comment; 2 | 3 | module("REST Adapter") ; 4 | 5 | //test("changing A=>null=>A should clean up the record", function() { 6 | //var store = DS.Store.create({ 7 | //adapter: DS.RESTAdapter 8 | //}); 9 | //var Kidney = DS.Model.extend(); 10 | //var Person = DS.Model.extend(); 11 | 12 | //Kidney.reopen({ 13 | //person: DS.belongsTo(Person) 14 | //}); 15 | //Kidney.toString = function() { return "Kidney"; }; 16 | 17 | //Person.reopen({ 18 | //name: DS.attr('string'), 19 | //kidneys: DS.hasMany(Kidney) 20 | //}); 21 | //Person.toString = function() { return "Person"; }; 22 | 23 | //store.load(Person, { id: 1, kidneys: [1, 2] }); 24 | //store.load(Kidney, { id: 1, person: 1 }); 25 | //store.load(Kidney, { id: 2, person: 1 }); 26 | 27 | //var person = store.find(Person, 1); 28 | //var kidney1 = store.find(Kidney, 1); 29 | //var kidney2 = store.find(Kidney, 2); 30 | 31 | //deepEqual(person.get('kidneys').toArray(), [kidney1, kidney2], "precond - person should have both kidneys"); 32 | //equal(kidney1.get('person'), person, "precond - first kidney should be in the person"); 33 | 34 | //person.get('kidneys').removeObject(kidney1); 35 | 36 | //ok(person.get('isDirty'), "precond - person should be dirty after operation"); 37 | //ok(kidney1.get('isDirty'), "precond - first kidney should be dirty after operation"); 38 | 39 | //deepEqual(person.get('kidneys').toArray(), [kidney2], "precond - person should have only the second kidney"); 40 | //equal(kidney1.get('person'), null, "precond - first kidney should be on the operating table"); 41 | 42 | //person.get('kidneys').addObject(kidney1); 43 | 44 | //ok(!person.get('isDirty'), "person should be clean after restoration"); 45 | //ok(!kidney1.get('isDirty'), "first kidney should be clean after restoration"); 46 | 47 | //deepEqual(person.get('kidneys').toArray(), [kidney2, kidney1], "person should have both kidneys again"); 48 | //equal(kidney1.get('person'), person, "first kidney should be in the person again"); 49 | //}); 50 | 51 | //test("changing A=>B=>A should clean up the record", function() { 52 | //var store = DS.Store.create({ 53 | //adapter: DS.RESTAdapter 54 | //}); 55 | //var Kidney = DS.Model.extend(); 56 | //var Person = DS.Model.extend(); 57 | 58 | //Kidney.reopen({ 59 | //person: DS.belongsTo(Person) 60 | //}); 61 | //Kidney.toString = function() { return "Kidney"; }; 62 | 63 | //Person.reopen({ 64 | //name: DS.attr('string'), 65 | //kidneys: DS.hasMany(Kidney) 66 | //}); 67 | //Person.toString = function() { return "Person"; }; 68 | 69 | //store.load(Person, { person: { id: 1, name: "John Doe", kidneys: [1, 2] }}); 70 | //store.load(Person, { person: { id: 2, name: "Jane Doe", kidneys: [3]} }); 71 | //store.load(Kidney, { kidney: { id: 1, person_id: 1 } }); 72 | //store.load(Kidney, { kidney: { id: 2, person_id: 1 } }); 73 | //store.load(Kidney, { kidney: { id: 3, person_id: 2 } }); 74 | 75 | //var john = store.find(Person, 1); 76 | //var jane = store.find(Person, 2); 77 | //var kidney1 = store.find(Kidney, 1); 78 | //var kidney2 = store.find(Kidney, 2); 79 | //var kidney3 = store.find(Kidney, 3); 80 | 81 | //deepEqual(john.get('kidneys').toArray(), [kidney1, kidney2], "precond - john should have the first two kidneys"); 82 | //deepEqual(jane.get('kidneys').toArray(), [kidney3], "precond - jane should have the third kidney"); 83 | //equal(kidney2.get('person'), john, "precond - second kidney should be in john"); 84 | 85 | //kidney2.set('person', jane); 86 | 87 | //ok(john.get('isDirty'), "precond - john should be dirty after operation"); 88 | //ok(jane.get('isDirty'), "precond - jane should be dirty after operation"); 89 | //ok(kidney2.get('isDirty'), "precond - second kidney should be dirty after operation"); 90 | 91 | //deepEqual(john.get('kidneys').toArray(), [kidney1], "precond - john should have only the first kidney"); 92 | //deepEqual(jane.get('kidneys').toArray(), [kidney3, kidney2], "precond - jane should have the other two kidneys"); 93 | //equal(kidney2.get('person'), jane, "precond - second kidney should be in jane"); 94 | 95 | //kidney2.set('person', john); 96 | 97 | //ok(!john.get('isDirty'), "john should be clean after restoration"); 98 | //ok(!jane.get('isDirty'), "jane should be clean after restoration"); 99 | //ok(!kidney2.get('isDirty'), "second kidney should be clean after restoration"); 100 | 101 | //deepEqual(john.get('kidneys').toArray(), [kidney1, kidney2], "john should have the first two kidneys again"); 102 | //deepEqual(jane.get('kidneys').toArray(), [kidney3], "jane should have the third kidney again"); 103 | //equal(kidney2.get('person'), john, "second kidney should be in john again"); 104 | //}); 105 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/embedded/embedded_dirtying_test.js: -------------------------------------------------------------------------------- 1 | var attr = DS.attr; 2 | var Post, Comment, User, Vote, Blog; 3 | var Adapter, App; 4 | var adapter, store, post; 5 | var forEach = Ember.EnumerableUtils.forEach; 6 | 7 | 8 | // The models here are related like this: 9 | // 10 | // Post 11 | // belongsTo / | 12 | // (non-embedded)| 13 | // Blog | hasMany 14 | // Comments 15 | // belongsTo / \ 16 | // / \ hasMany 17 | // User Votes 18 | 19 | module("Dirtying of Embedded Records", { 20 | setup: function() { 21 | App = Ember.Namespace.create({ name: "App" }); 22 | 23 | User = App.User = DS.Model.extend({ 24 | name: attr('string') 25 | }); 26 | 27 | Vote = App.Vote = DS.Model.extend({ 28 | voter: attr('string') 29 | }); 30 | 31 | Comment = App.Comment = DS.Model.extend({ 32 | title: attr('string'), 33 | user: DS.belongsTo(User), 34 | votes: DS.hasMany(Vote) 35 | }); 36 | 37 | Blog = App.Blog = DS.Model.extend({ 38 | title: attr('string') 39 | }); 40 | 41 | Post = App.Post = DS.Model.extend({ 42 | title: attr('string'), 43 | comments: DS.hasMany(Comment), 44 | blog: DS.belongsTo(Blog) 45 | }); 46 | 47 | Comment.reopen({ 48 | post: DS.belongsTo(Post) 49 | }); 50 | 51 | Adapter = DS.RESTAdapter.extend(); 52 | 53 | Adapter.map(Comment, { 54 | user: { embedded: 'always' }, 55 | votes: { embedded: 'always' } 56 | }); 57 | 58 | Adapter.map(Post, { 59 | comments: { embedded: 'always' }, 60 | blog: { embedded: 'load' } 61 | }); 62 | 63 | adapter = Adapter.create(); 64 | 65 | store = DS.Store.create({ 66 | adapter: adapter 67 | }); 68 | 69 | adapter.load(store, Post, { 70 | id: 1, 71 | title: "A New MVC Framework in Under 100 Lines of Code", 72 | 73 | blog: { 74 | id: 2, 75 | title: "Hacker News" 76 | }, 77 | 78 | comments: [{ 79 | title: "Why not use a more lightweight solution?", 80 | user: { 81 | name: "mongodb_user" 82 | }, 83 | votes: [ { voter: "tomdale" }, { voter: "wycats" } ] 84 | }, 85 | { 86 | title: "This does not seem to reflect the Unix philosophy haha", 87 | user: { 88 | name: "microuser", 89 | }, 90 | votes: [ { voter: "ebryn" } ] 91 | }] 92 | }); 93 | 94 | post = store.find(Post, 1); 95 | }, 96 | 97 | teardown: function() { 98 | store.destroy(); 99 | App.destroy(); 100 | } 101 | }); 102 | 103 | function assertEmbeddedLoadNotDirtied() { 104 | var blog = post.get('blog'); 105 | equal(blog.get('isDirty'), false, "embedded load records should not become dirty"); 106 | } 107 | 108 | function assertTreeIs(state) { 109 | post.get('comments').forEach(function(comment) { 110 | assertRecordIs(comment, state); 111 | if (comment.get('user')) { 112 | assertRecordIs(comment.get('user'), state); 113 | } 114 | comment.get('votes').forEach(function(vote) { 115 | assertRecordIs(vote, state); 116 | }); 117 | }); 118 | } 119 | 120 | function assertRecordIs(record, state) { 121 | var isDirty = state === 'dirty'; 122 | equal(record.get('isDirty'), isDirty, record.toString() + " should be " + state); 123 | } 124 | 125 | test("Modifying a record that contains embedded records should dirty the entire tree", function() { 126 | var post = store.find(Post, 1); 127 | post.set('title', "[dead]"); 128 | assertTreeIs('dirty'); 129 | assertEmbeddedLoadNotDirtied(); 130 | }); 131 | 132 | test("Modifying a record embedded via a belongsTo relationship should dirty the entire tree", function() { 133 | var user = post.get('comments.firstObject.user'); 134 | user.set('name', "[dead]"); 135 | assertTreeIs('dirty'); 136 | assertEmbeddedLoadNotDirtied(); 137 | }); 138 | 139 | test("Modifying a record embedded via a hasMany relationship should dirty the entire tree", function() { 140 | var vote = post.get('comments.firstObject.votes.firstObject'); 141 | vote.set('voter', "[dead]"); 142 | assertTreeIs('dirty'); 143 | }); 144 | 145 | test("Creating a record embedded via a hasMany relationship should dirty the entire tree", function() { 146 | var comment = store.createRecord(Comment, { 147 | post: post, 148 | title: 'A new comment' 149 | }); 150 | equal(comment.get('isDirty'), true, "New comment should be dirty"); 151 | assertTreeIs('dirty'); 152 | }); 153 | 154 | test("Creating a record embedded via a hasMany relationship should dirty the entire tree", function() { 155 | var comment = post.get('comments').createRecord({ title: 'A new comment' }); 156 | equal(comment.get('isDirty'), true, "New comment should be dirty"); 157 | assertTreeIs('dirty'); 158 | }); 159 | 160 | test("Modifyng a record embedded via embedded loading should not dirty the tree", function() { 161 | var blog = post.get('blog'); 162 | blog.set('title', "[dead]"); 163 | 164 | assertTreeIs('clean'); 165 | ok(blog.get('isDirty'), true, "embedded load record is dirty"); 166 | }); 167 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/embedded_polymorphic_has_many_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var originalLookup = Ember.lookup, lookup; 3 | var Adapter, adapter, serializer, store, App; 4 | 5 | module("Has-Many Polymorphic Relationships", { 6 | setup: function() { 7 | lookup = Ember.lookup = {}; 8 | serializer = DS.RESTSerializer.create(); 9 | Adapter = DS.RESTAdapter.extend({ 10 | serializer: serializer 11 | }); 12 | Adapter.configure('App.Comment', { 13 | alias: 'comment' 14 | }); 15 | 16 | Adapter.map('App.User', { 17 | messages: { embedded: 'always' } 18 | }); 19 | Adapter.configure('App.Post', { 20 | alias: 'post' 21 | }); 22 | Adapter.configure('App.Comment', { 23 | alias: 'comment' 24 | }); 25 | 26 | adapter = Adapter.create(); 27 | store = DS.Store.create({ 28 | adapter: adapter 29 | }); 30 | 31 | App = Ember.Namespace.create({ 32 | name: 'App' 33 | }); 34 | 35 | App.User = DS.Model.extend({ 36 | name: DS.attr('string'), 37 | messages: DS.hasMany('App.Message', {polymorphic: true}) 38 | }); 39 | 40 | App.Message = DS.Model.extend({ 41 | user: DS.belongsTo('App.User'), 42 | created_at: DS.attr('date') 43 | }); 44 | 45 | App.Post = App.Message.extend({ 46 | title: DS.attr('string'), 47 | comments: DS.hasMany('App.Comment') 48 | }); 49 | 50 | App.Comment = App.Message.extend({ 51 | body: DS.attr('string'), 52 | post: DS.belongsTo('App.Post') 53 | }); 54 | 55 | lookup.App = { 56 | Post: App.Post, 57 | Comment: App.Comment, 58 | User: App.User, 59 | Message: App.Message 60 | }; 61 | }, 62 | 63 | teardown: function() { 64 | serializer.destroy(); 65 | adapter.destroy(); 66 | store.destroy(); 67 | Ember.lookup = originalLookup; 68 | } 69 | }); 70 | 71 | 72 | test("A record can't be created from an embedded polymorphic hasMany relationship", function() { 73 | expect(1); 74 | store.load(App.User, { id: 1, messages: [] }); 75 | var user = store.find(App.User, 1), 76 | messages = user.get('messages'); 77 | 78 | raises( 79 | function() { messages.createRecord(); }, 80 | /You can not create records of App.Message on this polymorphic relationship/, 81 | "Creating records directly on a polymorphic hasMany is disallowed" 82 | ); 83 | }); 84 | 85 | test("Only records of the same base type can be added to an embedded polymorphic hasMany relationship", function() { 86 | expect(2); 87 | store.load(App.User, { id: 1 }); 88 | store.load(App.User, { id: 2 }); 89 | store.load(App.Post, { id: 1 }); 90 | store.load(App.Comment, { id: 3 }); 91 | 92 | var user = store.find(App.User, 1), 93 | anotherUser = store.find(App.User, 2), 94 | messages = user.get('messages'), 95 | post = store.find(App.Post, 1), 96 | comment = store.find(App.Comment, 3); 97 | 98 | messages.pushObject(post); 99 | messages.pushObject(comment); 100 | 101 | equal(messages.get('length'), 2, "The messages are correctly added"); 102 | 103 | raises( 104 | function() { messages.pushObject(anotherUser); }, 105 | /You can only add records of App.Message to this relationship/, 106 | "Adding records of a different base type on a polymorphic hasMany is disallowed" 107 | ); 108 | }); 109 | 110 | test("A record can be removed from an embedded polymorphic association", function() { 111 | expect(3); 112 | 113 | store.load(App.User, { id: 1 , messages: [{id: 3, type: 'comment'}]}); 114 | store.load(App.Comment, { id: 3 }); 115 | 116 | var user = store.find(App.User, 1), 117 | comment = store.find(App.Comment, 3), 118 | messages = user.get('messages'); 119 | 120 | equal(messages.get('length'), 1, "The user has 1 message"); 121 | 122 | var removedObject = messages.popObject(); 123 | 124 | equal(removedObject, comment, "The message is correctly removed"); 125 | equal(messages.get('length'), 0, "The user does not have any messages"); 126 | }); 127 | 128 | test("The store can load an embedded polymorphic hasMany association", function() { 129 | serializer.keyForEmbeddedType = function() { 130 | return 'embeddedType'; 131 | }; 132 | 133 | adapter.load(store, App.User, { id: 2, messages: [{ id: 1, embeddedType: 'comment'}]}); 134 | 135 | var user = store.find(App.User, 2), 136 | message = store.find(App.Comment, 1); 137 | 138 | deepEqual(user.get('messages').toArray(), [message]); 139 | }); 140 | 141 | test("The store can serialize an embedded polymorphic belongsTo association", function() { 142 | serializer.keyForEmbeddedType = function() { 143 | return 'embeddedType'; 144 | }; 145 | adapter.load(store, App.User, { id: 2, messages: [{ id: 1, embeddedType: 'comment'}]}); 146 | 147 | var user = store.find(App.User, 2), 148 | serialized = store.serialize(user, {includeId: true}); 149 | 150 | ok(serialized.hasOwnProperty('messages')); 151 | equal(serialized.messages.length, 1, "The messages are serialized"); 152 | equal(serialized.messages[0].id, 1); 153 | equal(serialized.messages[0].embeddedType, 'comment'); 154 | }); 155 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/inverse_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("Inverse test", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | }, 18 | 19 | teardown: function() { 20 | Ember.run(function() { 21 | store.destroy(); 22 | }); 23 | } 24 | }); 25 | 26 | test("One to one relationships should be identified correctly", function() { 27 | 28 | App.Post = DS.Model.extend({ 29 | title: DS.attr('string') 30 | }); 31 | 32 | App.Comment = DS.Model.extend({ 33 | body: DS.attr('string'), 34 | post: DS.belongsTo(App.Post) 35 | }); 36 | 37 | App.Post.reopen({ 38 | comment: DS.belongsTo(App.Comment) 39 | }); 40 | 41 | var type = DS.RelationshipChange.determineRelationshipType(App.Post, {key: "comment", kind: "belongsTo"}); 42 | 43 | equal(type, "oneToOne", "Relationship type is oneToOne"); 44 | }); 45 | 46 | test("One to many relationships should be identified correctly", function() { 47 | 48 | App.Post = DS.Model.extend({ 49 | title: DS.attr('string') 50 | }); 51 | 52 | App.Comment = DS.Model.extend({ 53 | body: DS.attr('string'), 54 | post: DS.hasMany(App.Post) 55 | }); 56 | 57 | App.Post.reopen({ 58 | comment: DS.belongsTo(App.Comment) 59 | }); 60 | 61 | var type = DS.RelationshipChange.determineRelationshipType(App.Post, {key: "comment", kind: "belongsTo"}); 62 | 63 | equal(type, "oneToMany", "Relationship type is oneToMany"); 64 | }); 65 | 66 | test("Many to one relationships should be identified correctly", function() { 67 | 68 | App.Post = DS.Model.extend({ 69 | title: DS.attr('string') 70 | }); 71 | 72 | App.Comment = DS.Model.extend({ 73 | body: DS.attr('string'), 74 | post: DS.hasMany(App.Post) 75 | }); 76 | 77 | App.Post.reopen({ 78 | comment: DS.belongsTo(App.Comment) 79 | }); 80 | 81 | var type = DS.RelationshipChange.determineRelationshipType(App.Comment, {key: "post", kind: "hasMany"}); 82 | 83 | equal(type, "manyToOne", "Relationship type is manyToOne"); 84 | }); 85 | 86 | test("Many to many relationships should be identified correctly", function() { 87 | 88 | App.Post = DS.Model.extend({ 89 | title: DS.attr('string') 90 | }); 91 | 92 | App.Comment = DS.Model.extend({ 93 | body: DS.attr('string'), 94 | post: DS.hasMany(App.Post) 95 | }); 96 | 97 | App.Post.reopen({ 98 | comment: DS.belongsTo(App.Comment) 99 | }); 100 | 101 | var type = DS.RelationshipChange.determineRelationshipType(App.Post, {key: "comment", kind: "hasMany"}); 102 | 103 | equal(type, "manyToMany", "Relationship type is manyTomany"); 104 | }); 105 | 106 | test("Many to none relationships should be identified correctly", function() { 107 | 108 | App.Post = DS.Model.extend({ 109 | title: DS.attr('string') 110 | }); 111 | 112 | App.Comment = DS.Model.extend({ 113 | body: DS.attr('string'), 114 | post: DS.hasMany(App.Post) 115 | }); 116 | 117 | var type = DS.RelationshipChange.determineRelationshipType(App.Comment, {key: "post", kind: "hasMany"}); 118 | 119 | equal(type, "manyToNone", "Relationship type is manyToNone"); 120 | }); 121 | 122 | test("One to none relationships should be identified correctly", function() { 123 | 124 | App.Post = DS.Model.extend({ 125 | title: DS.attr('string') 126 | }); 127 | 128 | App.Comment = DS.Model.extend({ 129 | body: DS.attr('string'), 130 | post: DS.belongsTo(App.Post) 131 | }); 132 | 133 | var type = DS.RelationshipChange.determineRelationshipType(App.Comment, {key: "post", kind: "belongsTo"}); 134 | 135 | equal(type, "oneToNone", "Relationship type is oneToNone"); 136 | }); 137 | 138 | test("A many relationship's inverse can be looked up", function() { 139 | App.Post = DS.Model.extend({ 140 | title: DS.attr('string') 141 | }); 142 | 143 | App.Comment = DS.Model.extend({ 144 | body: DS.attr('string'), 145 | post: DS.belongsTo(App.Post) 146 | }); 147 | 148 | App.Post.reopen({ 149 | comments: DS.hasMany(App.Comment) 150 | }); 151 | 152 | deepEqual(App.Post.inverseFor('comments'), { 153 | kind: 'belongsTo', 154 | type: App.Comment, 155 | name: 'post' 156 | }, "correct inverse descriptor was returned"); 157 | 158 | deepEqual(App.Comment.inverseFor('post'), { 159 | kind: 'hasMany', 160 | type: App.Post, 161 | name: 'comments' 162 | }); 163 | }); 164 | 165 | test("A many relationship's inverse can be looked up when it is part of a class hierarchy", function() { 166 | App.Post = DS.Model.extend({ 167 | title: DS.attr('string') 168 | }); 169 | 170 | App.Comment = DS.Model.extend({ 171 | body: DS.attr('string'), 172 | post: DS.belongsTo(App.Post) 173 | }); 174 | 175 | App.ChildComment = App.Comment.extend(); 176 | 177 | App.Post.reopen({ 178 | comments: DS.hasMany(App.Comment) 179 | }); 180 | 181 | deepEqual(App.ChildComment.inverseFor('post'), { 182 | kind: 'hasMany', 183 | type: App.Post, 184 | name: 'comments' 185 | }, "correct inverse descriptor was returned"); 186 | }); 187 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/belongs_to_test.js: -------------------------------------------------------------------------------- 1 | var Adapter, adapter, serializer, store, App; 2 | var originalLookup = Ember.lookup, lookup; 3 | var get = Ember.get, set = Ember.set; 4 | 5 | module("Belongs-To Relationships", { 6 | setup: function() { 7 | lookup = Ember.lookup = {}; 8 | 9 | serializer = DS.RESTSerializer.create(); 10 | Adapter = DS.RESTAdapter.extend({ 11 | serializer: serializer 12 | }); 13 | Adapter.configure('App.Comment', { 14 | alias: 'comment' 15 | }); 16 | Adapter.configure('App.Post', { 17 | alias: 'post' 18 | }); 19 | 20 | App = Ember.Namespace.create({ 21 | name: 'App' 22 | }); 23 | 24 | App.User = DS.Model.extend({ 25 | name: DS.attr('string'), 26 | messages: DS.hasMany('App.Message', {polymorphic: true}), 27 | favouriteMessage: DS.belongsTo('App.Message', {polymorphic: true}) 28 | }); 29 | 30 | App.Message = DS.Model.extend({ 31 | user: DS.belongsTo('App.User'), 32 | created_at: DS.attr('date') 33 | }); 34 | 35 | App.Post = App.Message.extend({ 36 | title: DS.attr('string'), 37 | comments: DS.hasMany('App.Comment') 38 | }); 39 | 40 | App.Comment = App.Message.extend({ 41 | body: DS.attr('string'), 42 | message: DS.belongsTo('App.Message', {polymorphic: true}) 43 | }); 44 | 45 | Adapter.map(App.User, { 46 | favouriteMessage: {embedded: 'always'} 47 | }); 48 | 49 | adapter = Adapter.create(); 50 | store = DS.Store.create({ 51 | adapter: adapter 52 | }); 53 | 54 | lookup.App = { 55 | User: App.User, 56 | Post: App.Post, 57 | Comment: App.Comment, 58 | Message: App.Message 59 | }; 60 | }, 61 | 62 | teardown: function() { 63 | serializer.destroy(); 64 | adapter.destroy(); 65 | store.destroy(); 66 | Ember.lookup = originalLookup; 67 | } 68 | }); 69 | 70 | test("The store can materialize a non loaded monomorphic belongsTo association", function() { 71 | expect(1); 72 | store.load(App.Post, { id: 1, user_id: 2}); 73 | var post = store.find(App.Post, 1); 74 | 75 | adapter.find = function(store, type, id) { 76 | ok(true, "The adapter's find method should be called"); 77 | }; 78 | 79 | post.get('user'); 80 | }); 81 | 82 | test("Only a record of the same type can be used with a monomorphic belongsTo relationship", function() { 83 | expect(1); 84 | store.load(App.Post, { id: 1 }); 85 | store.load(App.Comment, { id: 2 }); 86 | var post = store.find(App.Post, 1), 87 | comment = store.find(App.Comment, 2); 88 | 89 | raises( 90 | function() { post.set('user', comment); }, 91 | /You can only add a record of App.User to this relationship/, 92 | "Adding a record of a different type on a monomorphic belongsTo is disallowed" 93 | ); 94 | }); 95 | 96 | test("Only a record of the same base type can be used with a polymorphic belongsTo relationship", function() { 97 | expect(1); 98 | store.load(App.Comment, { id: 1 }); 99 | store.load(App.Comment, { id: 2 }); 100 | store.load(App.Post, { id: 1 }); 101 | store.load(App.User, { id: 3 }); 102 | 103 | var user = store.find(App.User, 3), 104 | post = store.find(App.Post, 1), 105 | comment = store.find(App.Comment, 1), 106 | anotherComment = store.find(App.Comment, 2); 107 | 108 | comment.set('message', anotherComment); 109 | comment.set('message', post); 110 | comment.set('message', null); 111 | 112 | raises( 113 | function() { comment.set('message', user); }, 114 | /You can only add a record of App.Message to this relationship/, 115 | "Adding a record of a different base type on a polymorphic belongsTo is disallowed" 116 | ); 117 | }); 118 | 119 | test("The store can load a polymorphic belongsTo association", function() { 120 | store.load(App.Post, { id: 1 }); 121 | store.load(App.Comment, { id: 2, message_id: 1, message_type: 'post' }); 122 | 123 | var message = store.find(App.Post, 1), 124 | comment = store.find(App.Comment, 2); 125 | 126 | equal(comment.get('message'), message); 127 | }); 128 | 129 | test("The store can serialize a polymorphic belongsTo association", function() { 130 | store.load(App.Post, { id: 1 }); 131 | store.load(App.Comment, { id: 2, message_id: 1, message_type: 'post' }); 132 | 133 | var comment = store.find(App.Comment, 2); 134 | 135 | var serialized = store.serialize(comment, {includeId: true}); 136 | equal(serialized.hasOwnProperty('message'), false); 137 | equal(serialized['message_id'], 1); 138 | equal(serialized['message_type'], 'post'); 139 | }); 140 | 141 | test("The store can load an embedded polymorphic belongsTo association", function() { 142 | serializer.keyForEmbeddedType = function() { 143 | return 'embeddedType'; 144 | }; 145 | 146 | adapter.load(store, App.User, { id: 2, favourite_message: { id: 1, embeddedType: 'comment'}}); 147 | 148 | var user = store.find(App.User, 2), 149 | message = store.find(App.Comment, 1); 150 | 151 | equal(user.get('favouriteMessage'), message); 152 | }); 153 | 154 | test("The store can serialize an embedded polymorphic belongsTo association", function() { 155 | serializer.keyForEmbeddedType = function() { 156 | return 'embeddedType'; 157 | }; 158 | adapter.load(store, App.User, { id: 2, favourite_message: { id: 1, embeddedType: 'comment'}}); 159 | 160 | var user = store.find(App.User, 2), 161 | serialized = store.serialize(user, {includeId: true}); 162 | 163 | ok(serialized.hasOwnProperty('favourite_message')); 164 | equal(serialized.favourite_message.id, 1); 165 | equal(serialized.favourite_message.embeddedType, 'comment'); 166 | }); 167 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/prematerialized_data_test.js: -------------------------------------------------------------------------------- 1 | var Person, adapter, serializer, store; 2 | 3 | module("Prematerialized Data", { 4 | setup: function() { 5 | Person = DS.Model.extend({ 6 | firstName: DS.attr('string'), 7 | lastName: DS.attr('string') 8 | }); 9 | 10 | Person.reopen({ 11 | bestBud: DS.belongsTo(Person), 12 | drugDealers: DS.hasMany(Person) 13 | }); 14 | 15 | adapter = DS.Adapter.create(), 16 | serializer = adapter.serializer, 17 | store = DS.Store.create({ adapter: adapter }); 18 | 19 | serializer.extractAttribute = function(type, data, name) { 20 | return data[name]; 21 | }; 22 | 23 | serializer.keyForAttributeName = function(type, name) { 24 | return name; 25 | }; 26 | }, 27 | 28 | teardown: function() { 29 | adapter.destroy(); 30 | store.destroy(); 31 | } 32 | }); 33 | 34 | test("after loading a record, a subsequent find materializes the record through the serializer", function() { 35 | expect(1); 36 | 37 | serializer.extractId = function(type, data) { 38 | ok(false, "extraction is skipped since the id was already provided"); 39 | }; 40 | 41 | store.load(Person, { id: 1, firstName: "Yehuda", lastName: "Katz" }, { id: 1 }); 42 | 43 | var person = store.find(Person, 1); 44 | equal(person.get('firstName'), "Yehuda", "getting an attribute returned the right value"); 45 | }); 46 | 47 | test("after loading a record with prematerialized attributes, a subsequent find materializes the record through the serializer", function() { 48 | expect(1); 49 | 50 | serializer.extractId = function(type, data) { 51 | ok(false, "extraction is skipped since the id was already provided"); 52 | }; 53 | 54 | store.load(Person, {}, { id: 1, firstName: "Yehuda", lastName: "Katz" }); 55 | 56 | var person = store.find(Person, 1); 57 | equal(person.get('firstName'), "Yehuda", "getting an attribute returned the right value"); 58 | }); 59 | 60 | test("after loading a record with a prematerialized belongsTo relationship, a subsequent find materializes the record through the serializer", function() { 61 | store.load(Person, {}, { id: 1, firstName: "Tom", lastName: "Dale", bestBud: 2 }); 62 | store.load(Person, {}, { id: 2, firstName: "Yehuda", lastName: "Katz", bestBud: 1 }); 63 | 64 | var tom = store.find(Person, 1); 65 | equal(tom.get('firstName'), "Tom", "attributes are materialized correctly"); 66 | var wycats = tom.get('bestBud'); 67 | 68 | equal(wycats.get('firstName'), "Yehuda", "related record was found successfully"); 69 | }); 70 | 71 | test("after loading a record with a prematerialized hasMany relationship, a subsequent find materializes the record through the serializer", function() { 72 | store.load(Person, {}, { id: 1, firstName: "Peter", lastName: "Wagenet" }); 73 | store.load(Person, {}, { id: 2, firstName: "Tom", lastName: "Dale" }); 74 | store.load(Person, {}, { id: 3, firstName: "Yehuda", lastName: "Katz", drugDealers: [1, 2] }); 75 | 76 | var wycats = store.find(Person, 3); 77 | equal(wycats.get('firstName'), "Yehuda", "attributes are materialized correctly"); 78 | 79 | var drugDealers = wycats.get('drugDealers'); 80 | 81 | equal(drugDealers.objectAt(0).get('firstName'), "Peter", "first drug dealer is found"); 82 | equal(drugDealers.objectAt(1).get('firstName'), "Tom", "second drug dealer is found"); 83 | }); 84 | 85 | asyncTest("if a record is found through store.find(), its prematerialized attributes are loaded once the adapter returns", function() { 86 | expect(1); 87 | 88 | serializer.extractId = function(type, data) { 89 | ok(false, "extraction is skipped since the id was already provided"); 90 | }; 91 | 92 | adapter.find = function(store, type, id) { 93 | setTimeout(function() { 94 | store.load(Person, {}, { id: 1, firstName: "Yehuda", lastName: "Katz" }); 95 | equal(person.get('firstName'), "Yehuda", "getting an attribute returned the right value"); 96 | start(); 97 | }); 98 | }; 99 | var person = store.find(Person, 1); 100 | }); 101 | 102 | asyncTest("if a record is found through store.find(), its prematerialized belongsTo is loaded once the adapter returns", function() { 103 | adapter.find = function(store, type, id) { 104 | setTimeout(function() { 105 | store.load(Person, {}, { id: 1, firstName: "Tom", lastName: "Dale", bestBud: 2 }); 106 | store.load(Person, {}, { id: 2, firstName: "Yehuda", lastName: "Katz", bestBud: 1 }); 107 | 108 | var tom = store.find(Person, 1); 109 | equal(tom.get('firstName'), "Tom", "attributes are materialized correctly"); 110 | var wycats = tom.get('bestBud'); 111 | 112 | equal(wycats.get('firstName'), "Yehuda", "related record was found successfully"); 113 | start(); 114 | }); 115 | }; 116 | var person = store.find(Person, 1); 117 | }); 118 | 119 | asyncTest("if a record is found through store.find(), its prematerialized hasMany is loaded once the adapter returns", function() { 120 | adapter.find = function(store, type, id) { 121 | setTimeout(function() { 122 | store.load(Person, {}, { id: 1, firstName: "Peter", lastName: "Wagenet" }); 123 | store.load(Person, {}, { id: 2, firstName: "Tom", lastName: "Dale" }); 124 | store.load(Person, {}, { id: 3, firstName: "Yehuda", lastName: "Katz", drugDealers: [1, 2] }); 125 | 126 | var wycats = store.find(Person, 3); 127 | equal(wycats.get('firstName'), "Yehuda", "attributes are materialized correctly"); 128 | 129 | var drugDealers = wycats.get('drugDealers'); 130 | 131 | equal(drugDealers.objectAt(0).get('firstName'), "Peter", "first drug dealer is found"); 132 | equal(drugDealers.objectAt(1).get('firstName'), "Tom", "second drug dealer is found"); 133 | 134 | start(); 135 | }); 136 | }; 137 | var person = store.find(Person, 3); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/transform_test.js: -------------------------------------------------------------------------------- 1 | var Adapter, adapter, store, serializer, Person; 2 | 3 | module("Record Attribute Transforms", { 4 | setup: function() { 5 | Adapter = DS.Adapter.extend(); 6 | 7 | Adapter.registerTransform('unobtainium', { 8 | serialize: function(value) { 9 | return 'serialize'; 10 | }, 11 | 12 | deserialize: function(value) { 13 | return 'fromData'; 14 | } 15 | }); 16 | 17 | adapter = Adapter.create(); 18 | store = DS.Store.create({ 19 | adapter: adapter 20 | }); 21 | serializer = adapter.get('serializer'); 22 | }, 23 | 24 | teardown: function() { 25 | serializer.destroy(); 26 | adapter.destroy(); 27 | store.destroy(); 28 | } 29 | }); 30 | 31 | test("transformed values should be materialized on the record", function() { 32 | var Person = DS.Model.extend({ 33 | name: DS.attr('unobtainium') 34 | }); 35 | 36 | store.load(Person, { id: 1, name: "James Cameron" }); 37 | 38 | var person = store.find(Person, 1); 39 | equal(person.get('name'), 'fromData', "value of attribute on the record should be transformed"); 40 | 41 | var json = adapter.serialize(person); 42 | equal(json.name, "serialize", "value of attribute in the JSON hash should be transformed"); 43 | }); 44 | 45 | module("Default DS.Transforms", { 46 | setup: function() { 47 | store = DS.Store.create(); 48 | 49 | Person = DS.Model.extend({ 50 | name: DS.attr('string'), 51 | born: DS.attr('date'), 52 | age: DS.attr('number'), 53 | isGood: DS.attr('boolean') 54 | }); 55 | }, 56 | 57 | teardown: function() { 58 | store.destroy(); 59 | } 60 | }); 61 | 62 | test("the default numeric transform", function() { 63 | store.load(Person, {id: 1, age: "51"}); 64 | var person = store.find(Person, 1); 65 | 66 | var result = (typeof person.get('age') === "number"); 67 | equal(result, true, "string is transformed into a number"); 68 | equal(person.get('age'),51, "string value and transformed numeric value match"); 69 | }); 70 | 71 | test("the default boolean transform", function() { 72 | store.load(Person, {id: 1, isGood: "false"}); 73 | store.load(Person, {id: 2, isGood: "f"}); 74 | store.load(Person, {id: 3, isGood: 0}); 75 | store.load(Person, {id: 4, isGood: false}); 76 | 77 | var personOne = store.find(Person, 1); 78 | var personTwo = store.find(Person, 2); 79 | var personThree = store.find(Person, 3); 80 | var personFour = store.find(Person, 4); 81 | 82 | var result = (typeof personOne.get('isGood') === "boolean"); 83 | equal(result, true, "string is transformed into a boolean"); 84 | 85 | equal(personOne.get('isGood'), false, "string value and transformed boolean value match"); 86 | equal(personTwo.get('isGood'), false, "short string value and transformed boolean value match"); 87 | equal(personThree.get('isGood'), false, "numeric value and transformed boolean value match"); 88 | equal(personFour.get('isGood'), false, "boolean value and transformed boolean value match"); 89 | }); 90 | 91 | test("the default string transform", function() { 92 | store.load(Person, {id: 1, name: 8675309}); 93 | var person = store.find(Person, 1); 94 | 95 | var result = (typeof person.get('name') === "string"); 96 | equal(result, true, "number is transformed into a string"); 97 | equal(person.get('name'), "8675309", "numeric value and transformed string value match"); 98 | }); 99 | 100 | test("the default date transform", function() { 101 | var date = new Date(); 102 | store.load(Person, {id: 1, born: date.toString()}); 103 | var person = store.find(Person, 1); 104 | 105 | var result = (person.get('born') instanceof Date); 106 | equal(result, true, "string is transformed into a date"); 107 | equal(person.get('born').toString(), date.toString(), "date.toString and transformed date.toString values match"); 108 | 109 | var timestamp = 293810400, // 1979-04-24 @ 08:00:00 110 | date2 = new Date(timestamp); 111 | 112 | store.load(Person, {id: 2, born: timestamp}); 113 | var person2 = store.find(Person, 2); 114 | 115 | var result2 = (person.get('born') instanceof Date); 116 | equal(result2, true, "timestamp is transformed into a date"); 117 | equal(person2.get('born').toString(), date2.toString(), "date.toString and transformed date.toString values match"); 118 | }); 119 | 120 | test("the date transform parses iso8601 dates", function() { 121 | var expectDate = function(string, timestamp, message) { 122 | equal(Ember.Date.parse(string), timestamp, message); 123 | }; 124 | 125 | expectDate('2011-11-29T15:52:18.867', 1322581938867, "YYYY-MM-DDTHH:mm:ss.sss"); 126 | expectDate('2011-11-29T15:52:18.867Z', 1322581938867, "YYYY-MM-DDTHH:mm:ss.sssZ"); 127 | expectDate('2011-11-29T15:52:18.867-03:30', 1322594538867, "YYYY-MM-DDTHH:mm:ss.sss-HH:mm"); 128 | expectDate('2011-11-29', 1322524800000, "YYYY-MM-DD"); 129 | expectDate('2011-11', 1320105600000, "YYYY-MM"); 130 | expectDate('2011', 1293840000000, "YYYY"); 131 | }); 132 | 133 | module("Enum Transforms", { 134 | setup: function() { 135 | adapter = DS.Adapter.create(); 136 | adapter.registerEnumTransform('materials', ['unobtainium', 'kindaobtainium', 'veryobtainium']); 137 | 138 | store = DS.Store.create({ 139 | adapter: adapter 140 | }); 141 | 142 | serializer = adapter.get('serializer'); 143 | 144 | Person = DS.Model.extend({ 145 | material: DS.attr('materials') 146 | }); 147 | }, 148 | teardown: function() { 149 | serializer.destroy(); 150 | adapter.destroy(); 151 | store.destroy(); 152 | } 153 | }); 154 | 155 | test("correct transforms are applied", function() { 156 | var json, person; 157 | store.load(Person, { 158 | id: 1, 159 | material: 2 160 | }); 161 | 162 | person = store.find(Person, 1); 163 | equal(person.get('material'), 'veryobtainium', 'value of the attribute on the record should be transformed'); 164 | 165 | json = adapter.serialize(person); 166 | equal(json.material, 2, 'value of the attribute in the JSON hash should be transformed'); 167 | }); 168 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/materialization_tests.js: -------------------------------------------------------------------------------- 1 | var Person; 2 | var store; 3 | var adapter; 4 | var serializer; 5 | 6 | module("Record Materialization", { 7 | setup: function() { 8 | Person = DS.Model.extend({ 9 | updatedAt: DS.attr('string'), 10 | name: DS.attr('string'), 11 | firstName: DS.attr('string'), 12 | lastName: DS.attr('string') 13 | }); 14 | 15 | serializer = DS.JSONSerializer.create(); 16 | adapter = DS.Adapter.create({ 17 | serializer: serializer 18 | }); 19 | store = DS.Store.create({ adapter: adapter }); 20 | }, 21 | 22 | teardown: function() { 23 | adapter.destroy(); 24 | store.destroy(); 25 | } 26 | }); 27 | 28 | test("the adapter's materialize method should provide attributes to a record", function() { 29 | store.load(Person, { id: 1, FIRST_NAME: "Yehuda", lAsTnAmE: "Katz" }); 30 | 31 | adapter.materialize = function(record, hash) { 32 | record.materializeAttributes({ 33 | firstName: hash.FIRST_NAME, 34 | lastName: hash.lAsTnAmE 35 | }); 36 | }; 37 | 38 | var person = store.find(Person, 1); 39 | 40 | equal(person.get('firstName'), "Yehuda"); 41 | equal(person.get('lastName'), "Katz"); 42 | }); 43 | 44 | test("when materializing a record, the serializer's materializeAttributes method should be invoked", function() { 45 | expect(1); 46 | 47 | store.load(Person, { id: 1, FIRST_NAME: "Yehuda", lAsTnAmE: "Katz" }); 48 | 49 | serializer.materializeAttributes = function(record, hash) { 50 | deepEqual(hash, { 51 | id: 1, 52 | FIRST_NAME: "Yehuda", 53 | lAsTnAmE: "Katz" 54 | }); 55 | }; 56 | 57 | var person = store.find(Person, 1); 58 | }); 59 | 60 | test("when materializing a record, the serializer's materializeAttribute method should be invoked for each attribute", function() { 61 | expect(8); 62 | 63 | store.load(Person, { id: 1, FIRST_NAME: "Yehuda", lAsTnAmE: "Katz" }); 64 | 65 | var attributes = { 66 | firstName: 'string', 67 | lastName: 'string', 68 | updatedAt: 'string', 69 | name: 'string' 70 | }; 71 | 72 | serializer.materializeAttribute = function(record, hash, attributeName, attributeType) { 73 | deepEqual(hash, { 74 | id: 1, 75 | FIRST_NAME: "Yehuda", 76 | lAsTnAmE: "Katz" 77 | }); 78 | 79 | var expectedType = attributes[attributeName]; 80 | equal(expectedType, attributeType, "The attribute type should be correct"); 81 | delete attributes[attributeName]; 82 | }; 83 | 84 | var person = store.find(Person, 1); 85 | }); 86 | 87 | test("extractId is called when loading a record but not when materializing it afterwards", function() { 88 | expect(2); 89 | 90 | serializer.extractId = function(type, hash) { 91 | equal(type, Person, "extractId is passed the correct type"); 92 | deepEqual(hash, { id: 1, name: "Yehuda Katz" }, "the opaque hash should be passed"); 93 | 94 | return 1; 95 | }; 96 | 97 | store.load(Person, { id: 1, name: "Yehuda Katz" }); 98 | 99 | // Find record to ensure it gets materialized 100 | var person = store.find(Person, 1); 101 | }); 102 | 103 | test("when materializing a record, the serializer's extractAttribute is called for each attribute defined on the model", function() { 104 | expect(9); 105 | 106 | var DrugDealer = DS.Model.extend({ 107 | firstName: DS.attr('string'), 108 | lastName: DS.attr('string'), 109 | yearsIncarcerated: DS.attr('number') 110 | }); 111 | 112 | // Keep a hash of which attribute names extractAttribute 113 | // has been called with, and `tick` them off as we go along. 114 | var attributes = { 115 | firstName: true, 116 | lastName: true, 117 | yearsIncarcerated: true 118 | }; 119 | 120 | store.load(DrugDealer, { id: 1, firstName: "Patrick", lastName: "Gibson", yearsIncarcerated: 42 }); 121 | 122 | serializer.extractAttribute = function(type, hash, attributeName) { 123 | deepEqual(hash, { id: 1, firstName: "Patrick", lastName: "Gibson", yearsIncarcerated: 42 }, "opaque hash should be passed to extractAttribute"); 124 | equal(type, DrugDealer, "model type is passed to extractAttribute"); 125 | 126 | ok(attributes.hasOwnProperty(attributeName), "the attribute name is present"); 127 | delete attributes[attributeName]; 128 | }; 129 | 130 | store.find(DrugDealer, 1); 131 | }); 132 | 133 | test("when materializing a record, the serializer's extractHasMany method should be invoked", function() { 134 | expect(3); 135 | 136 | Person.reopen({ 137 | children: DS.hasMany(Person) 138 | }); 139 | 140 | store.load(Person, { id: 1, children: [ 1, 2, 3 ] }); 141 | 142 | serializer.extractHasMany = function(type, hash, name) { 143 | equal(type, Person); 144 | deepEqual(hash, { 145 | id: 1, 146 | children: [ 1, 2, 3 ] 147 | }); 148 | equal(name, 'children'); 149 | }; 150 | 151 | var person = store.find(Person, 1); 152 | }); 153 | 154 | test("when materializing a record, the serializer's extractBelongsTo method should be invoked", function() { 155 | expect(3); 156 | 157 | Person.reopen({ 158 | father: DS.belongsTo(Person) 159 | }); 160 | 161 | store.load(Person, { id: 1, father: 2 }); 162 | 163 | serializer.extractBelongsTo = function(type, hash, name) { 164 | equal(type, Person); 165 | deepEqual(hash, { 166 | id: 1, 167 | father: 2 168 | }); 169 | equal(name, 'father'); 170 | }; 171 | 172 | var person = store.find(Person, 1); 173 | }); 174 | 175 | test("when materializing a record, deserializeValue is called to convert the value from data into a JavaScript value", function() { 176 | expect(2); 177 | 178 | var Bowler = DS.Model.extend({ 179 | favoriteDrink: DS.attr('string'), 180 | hasSpecialLadyFriend: DS.attr('boolean') 181 | }); 182 | 183 | var typeToValueMap = { 184 | "string": "white russian", 185 | "boolean": "FALSE" 186 | }; 187 | 188 | store.load(Bowler, { id: 'dude', favoriteDrink: "white russian", hasSpecialLadyFriend: "FALSE" }); 189 | serializer.deserializeValue = function(value, attributeType) { 190 | strictEqual(typeToValueMap[attributeType], value, "correct value and type pair should be passed"); 191 | delete typeToValueMap[attributeType]; 192 | 193 | return value; 194 | }; 195 | 196 | store.find(Bowler, 'dude'); 197 | }); 198 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/dirtiness_test.js: -------------------------------------------------------------------------------- 1 | var store, adapter; 2 | var Person; 3 | 4 | module("Attribute Changes and Dirtiness", { 5 | setup: function() { 6 | adapter = DS.Adapter.create(); 7 | 8 | store = DS.Store.create({ 9 | adapter: adapter 10 | }); 11 | 12 | Person = DS.Model.extend({ 13 | firstName: DS.attr('string') 14 | }); 15 | } 16 | }); 17 | 18 | test("By default, if a record's attribute is changed, it becomes dirty", function() { 19 | store.load(Person, { id: 1, firstName: "Yehuda" }); 20 | var wycats = store.find(Person, 1); 21 | 22 | wycats.set('firstName', "Brohuda"); 23 | 24 | ok(wycats.get('isDirty'), "record has become dirty"); 25 | }); 26 | 27 | test("By default, a newly created record is dirty", function() { 28 | var wycats = store.createRecord(Person); 29 | 30 | ok(wycats.get('isDirty'), "record is dirty"); 31 | }); 32 | 33 | test("By default, changing the relationship between two records does not cause them to become dirty", function() { 34 | adapter.dirtyRecordsForHasManyChange = Ember.K; 35 | adapter.dirtyRecordsForBelongsToChange = Ember.K; 36 | 37 | var Post = DS.Model.extend(); 38 | 39 | var Comment = DS.Model.extend({ 40 | post: DS.belongsTo(Post) 41 | }); 42 | 43 | Post.reopen({ 44 | comments: DS.hasMany(Comment) 45 | }); 46 | 47 | store.load(Post, { id: 1, comments: [1] }); 48 | store.load(Comment, { id: 1, post: 1 }); 49 | 50 | var post = store.find(Post, 1); 51 | var comment = store.find(Comment, 1); 52 | 53 | comment.set('post', null); 54 | 55 | ok(!post.get('isDirty'), "post should not be dirty"); 56 | ok(!comment.get('isDirty'), "comment should not be dirty"); 57 | }); 58 | 59 | test("If dirtyRecordsForAttributeChange does not add the record to the dirtyRecords set, it does not become dirty", function() { 60 | store.load(Person, { id: 1, firstName: "Yehuda" }); 61 | var wycats = store.find(Person, 1); 62 | 63 | adapter.dirtyRecordsForAttributeChange = function(dirtyRecords, changedRecord, attributeName) { 64 | equal(changedRecord, wycats, "changed record is passed to hook"); 65 | equal(attributeName, "firstName", "attribute name is passed to hook"); 66 | }; 67 | 68 | wycats.set('firstName', "Brohuda"); 69 | 70 | ok(!wycats.get('isDirty'), "the record is not dirty despite attribute change"); 71 | }); 72 | 73 | test("If dirtyRecordsForAttributeChange adds the record to the dirtyRecords set, it becomes dirty", function() { 74 | store.load(Person, { id: 1, firstName: "Yehuda" }); 75 | var wycats = store.find(Person, 1); 76 | 77 | adapter.dirtyRecordsForAttributeChange = function(dirtyRecords, changedRecord, attributeName) { 78 | equal(changedRecord, wycats, "changed record is passed to hook"); 79 | equal(attributeName, "firstName", "attribute name is passed to hook"); 80 | dirtyRecords.add(changedRecord); 81 | }; 82 | 83 | wycats.set('firstName', "Brohuda"); 84 | 85 | ok(wycats.get('isDirty'), "the record is dirty after attribute change"); 86 | }); 87 | 88 | test("If dirtyRecordsForAttributeChange adds a different record than the changed record to the dirtyRecords set, the different record becomes dirty", function() { 89 | store.load(Person, { id: 1, firstName: "Yehuda" }); 90 | store.load(Person, { id: 2, firstName: "Tom" }); 91 | var wycats = store.find(Person, 1); 92 | var tomdale = store.find(Person, 2); 93 | 94 | adapter.dirtyRecordsForAttributeChange = function(dirtyRecords, changedRecord, attributeName) { 95 | equal(changedRecord, wycats, "changed record is passed to hook"); 96 | equal(attributeName, "firstName", "attribute name is passed to hook"); 97 | dirtyRecords.add(tomdale); 98 | }; 99 | 100 | wycats.set('firstName', "Brohuda"); 101 | 102 | ok(tomdale.get('isDirty'), "the record is dirty after attribute change"); 103 | ok(!wycats.get('isDirty'), "the record is not dirty after attribute change"); 104 | }); 105 | 106 | test("If dirtyRecordsForAttributeChange adds two records to the dirtyRecords set, both become dirty", function() { 107 | store.load(Person, { id: 1, firstName: "Yehuda" }); 108 | store.load(Person, { id: 2, firstName: "Tom" }); 109 | var wycats = store.find(Person, 1); 110 | var tomdale = store.find(Person, 2); 111 | 112 | adapter.dirtyRecordsForAttributeChange = function(dirtyRecords, changedRecord, attributeName) { 113 | equal(changedRecord, wycats, "changed record is passed to hook"); 114 | equal(attributeName, "firstName", "attribute name is passed to hook"); 115 | dirtyRecords.add(tomdale); 116 | dirtyRecords.add(wycats); 117 | }; 118 | 119 | wycats.set('firstName', "Brohuda"); 120 | 121 | ok(tomdale.get('isDirty'), "the record is dirty after attribute change"); 122 | ok(wycats.get('isDirty'), "the record is dirty after attribute change"); 123 | }); 124 | 125 | test("When adding a newly created record to a hasMany relationship, the parent should become clean after committing", function() { 126 | var App = Ember.Namespace.create(); 127 | App.toString = function() { return "App"; }; 128 | 129 | App.Post = DS.Model.extend({ 130 | title: DS.attr('string') 131 | }); 132 | 133 | App.Comment = DS.Model.extend({ 134 | body: DS.attr('string'), 135 | post: DS.belongsTo(App.Post) 136 | }); 137 | 138 | App.Post.reopen({ 139 | comments: DS.hasMany(App.Comment) 140 | }); 141 | 142 | expect(3); 143 | 144 | adapter.dirtyRecordsForHasManyChange = Ember.K; 145 | 146 | function didSaveRecord(store, record, hash) { 147 | record.eachRelationship(function(name, relationship) { 148 | if (relationship.kind === 'belongsTo') { 149 | store.didUpdateRelationship(record, name); 150 | } 151 | }); 152 | 153 | store.didSaveRecord(record, hash); 154 | } 155 | 156 | adapter.createRecord = function(store, type, record) { 157 | didSaveRecord(store, record, this.serialize(record)); 158 | }; 159 | 160 | store.load(App.Post, { id: 1}); 161 | var post = store.find(App.Post, 1); 162 | 163 | post.get('comments').createRecord(); 164 | 165 | equal(post.get('isDirty'), false, "precond - the record should be dirty"); 166 | 167 | store.commit(); 168 | 169 | equal(post.get('isDirty'), false, "The record should no longer be dirty"); 170 | equal(post.get('isSaving'), false, "The record should no longer be saving"); 171 | }); 172 | -------------------------------------------------------------------------------- /packages/ember-data/lib/system/record_arrays/many_array.js: -------------------------------------------------------------------------------- 1 | require("ember-data/system/record_arrays/record_array"); 2 | 3 | /** 4 | @module data 5 | @submodule data-record-array 6 | */ 7 | 8 | var get = Ember.get, set = Ember.set; 9 | 10 | /** 11 | A ManyArray is a RecordArray that represents the contents of a has-many 12 | relationship. 13 | 14 | The ManyArray is instantiated lazily the first time the relationship is 15 | requested. 16 | 17 | ### Inverses 18 | 19 | Often, the relationships in Ember Data applications will have 20 | an inverse. For example, imagine the following models are 21 | defined: 22 | 23 | App.Post = DS.Model.extend({ 24 | comments: DS.hasMany('App.Comment') 25 | }); 26 | 27 | App.Comment = DS.Model.extend({ 28 | post: DS.belongsTo('App.Post') 29 | }); 30 | 31 | If you created a new instance of `App.Post` and added 32 | a `App.Comment` record to its `comments` has-many 33 | relationship, you would expect the comment's `post` 34 | property to be set to the post that contained 35 | the has-many. 36 | 37 | We call the record to which a relationship belongs the 38 | relationship's _owner_. 39 | 40 | @class ManyArray 41 | @namespace DS 42 | @extends DS.RecordArray 43 | @constructor 44 | */ 45 | DS.ManyArray = DS.RecordArray.extend({ 46 | init: function() { 47 | this._super.apply(this, arguments); 48 | this._changesToSync = Ember.OrderedSet.create(); 49 | }, 50 | 51 | /** 52 | @private 53 | 54 | The record to which this relationship belongs. 55 | 56 | @property {DS.Model} 57 | */ 58 | owner: null, 59 | 60 | /** 61 | @private 62 | 63 | `true` if the relationship is polymorphic, `false` otherwise. 64 | 65 | @property {Boolean} 66 | */ 67 | isPolymorphic: false, 68 | 69 | // LOADING STATE 70 | 71 | isLoaded: false, 72 | 73 | loadingRecordsCount: function(count) { 74 | this.loadingRecordsCount = count; 75 | }, 76 | 77 | loadedRecord: function() { 78 | this.loadingRecordsCount--; 79 | if (this.loadingRecordsCount === 0) { 80 | set(this, 'isLoaded', true); 81 | this.trigger('didLoad'); 82 | } 83 | }, 84 | 85 | fetch: function() { 86 | var references = get(this, 'content'), 87 | store = get(this, 'store'), 88 | owner = get(this, 'owner'); 89 | 90 | store.fetchUnloadedReferences(references, owner); 91 | }, 92 | 93 | // Overrides Ember.Array's replace method to implement 94 | replaceContent: function(index, removed, added) { 95 | // Map the array of record objects into an array of client ids. 96 | added = added.map(function(record) { 97 | Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this relationship.", !get(this, 'type') || (get(this, 'type').detectInstance(record)) ); 98 | return get(record, '_reference'); 99 | }, this); 100 | 101 | this._super(index, removed, added); 102 | }, 103 | 104 | arrangedContentDidChange: function() { 105 | this.fetch(); 106 | }, 107 | 108 | arrayContentWillChange: function(index, removed, added) { 109 | var owner = get(this, 'owner'), 110 | name = get(this, 'name'); 111 | 112 | if (!owner._suspendedRelationships) { 113 | // This code is the first half of code that continues inside 114 | // of arrayContentDidChange. It gets or creates a change from 115 | // the child object, adds the current owner as the old 116 | // parent if this is the first time the object was removed 117 | // from a ManyArray, and sets `newParent` to null. 118 | // 119 | // Later, if the object is added to another ManyArray, 120 | // the `arrayContentDidChange` will set `newParent` on 121 | // the change. 122 | for (var i=index; i -1, message); 90 | }; 91 | 92 | test("Transformations registered on an adapter class should be set on the adapter's serializer at initialization time.", function() { 93 | // Make sure that transformations on parent adapter classes are included 94 | // if subclasses are created. 95 | 96 | var Adapter = DS.Adapter.extend(); 97 | 98 | var parentUnobtainium = { 99 | serialize: function(value) { 100 | return 'serialize'; 101 | }, 102 | 103 | fromData: function(value) { 104 | return 'fromData'; 105 | } 106 | }; 107 | 108 | Adapter.registerTransform('unobtainium', parentUnobtainium); 109 | 110 | var ChildAdapter = Adapter.extend(); 111 | 112 | var childAdamantium = { 113 | serialize: function(value) { 114 | return 'adamantium serialize'; 115 | }, 116 | 117 | fromData: function(value) { 118 | return 'adamantium fromData'; 119 | } 120 | }; 121 | 122 | ChildAdapter.registerTransform('adamantium', childAdamantium); 123 | 124 | var parentOtherType = { 125 | serialize: function(value) { 126 | return 'otherType serialize'; 127 | }, 128 | 129 | fromData: function(value) { 130 | return 'otherType fromData'; 131 | } 132 | }; 133 | 134 | Adapter.registerTransform('otherType', parentOtherType); 135 | 136 | ChildAdapter.create({ 137 | serializer: serializerMock 138 | }); 139 | 140 | deepEqual(transformsPassed, { 141 | unobtainium: parentUnobtainium, 142 | adamantium: childAdamantium, 143 | otherType: parentOtherType 144 | }); 145 | }); 146 | 147 | test("Transforms registered subclasses take precedence over super classes.", function() { 148 | var ParentAdapter = DS.Adapter.extend(); 149 | var ChildAdapter = ParentAdapter.extend(); 150 | 151 | var childUnobtainium = { 152 | serialize: Ember.K, 153 | fromData: Ember.K 154 | }; 155 | 156 | var parentUnobtainium = { 157 | serialize: Ember.K, 158 | fromData: Ember.K 159 | }; 160 | 161 | ChildAdapter.registerTransform('unobtainium', childUnobtainium); 162 | ParentAdapter.registerTransform('unobtainium', parentUnobtainium); 163 | 164 | ChildAdapter.create({ 165 | serializer: serializerMock 166 | }); 167 | 168 | deepEqual(transformsPassed, { 169 | unobtainium: childUnobtainium 170 | }); 171 | }); 172 | 173 | var mappingsPassed; 174 | 175 | module("DS.Adapter - Mapping", { 176 | setup: function() { 177 | mappingsPassed = {}; 178 | 179 | serializerMock = Ember.Object.create({ 180 | map: function(type, mappings) { 181 | var mappingsForType = mappingsPassed[type] = mappingsPassed[type] || {}; 182 | 183 | for (var prop in mappings) { 184 | if (!mappings.hasOwnProperty(prop)) { continue; } 185 | 186 | mappingsForType[prop] = mappings[prop]; 187 | } 188 | } 189 | }); 190 | }, 191 | 192 | teardown: function() { 193 | serializerMock.destroy(); 194 | } 195 | }); 196 | 197 | test("Mappings registered on an adapter class should be set on the adapter's serializer at initialization time.", function() { 198 | var Adapter = DS.Adapter.extend(); 199 | var oldLookup = Ember.lookup; 200 | Ember.lookup = { 201 | App: {} 202 | }; 203 | 204 | Ember.lookup.App.Person = Ember.Object.extend(); 205 | 206 | Adapter.map('App.Person', { 207 | firstName: { key: 'FIRST_NAME' } 208 | }); 209 | 210 | var ChildAdapter = Adapter.extend(); 211 | 212 | ChildAdapter.map('App.Person', { 213 | lastName: { key: 'LAST_NAME' } 214 | }); 215 | 216 | Adapter.map('App.Person', { 217 | middleName: { key: 'MIDDLE_NAME' }, 218 | lastName: { key: 'SHOULD_NOT_WORK' } 219 | }); 220 | 221 | ChildAdapter.create({ 222 | serializer: serializerMock 223 | }); 224 | 225 | deepEqual(mappingsPassed, { 226 | 'App.Person': { 227 | firstName: { key: 'FIRST_NAME' }, 228 | lastName: { key: 'LAST_NAME' }, 229 | middleName: { key: 'MIDDLE_NAME' } 230 | } 231 | }); 232 | 233 | Ember.lookup = oldLookup; 234 | }); 235 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/fixture_adapter_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var App, ComplexObject, Person, store, adapter; 3 | 4 | ComplexObject = Ember.Object.extend({ 5 | 6 | }); 7 | 8 | module("DS.FixtureAdapter & DS.FixtureSerializer", { 9 | setup: function() { 10 | App = Ember.Namespace.create(); 11 | 12 | App.Person = DS.Model.extend({ 13 | name: DS.attr('string'), 14 | profile: DS.attr('object'), 15 | }); 16 | 17 | App.Person.FIXTURES = []; 18 | 19 | App.User = App.Person.extend({ 20 | messages: DS.hasMany('App.Message', {polymorphic: true}) 21 | }); 22 | App.Admin = App.User.extend({}); 23 | 24 | App.Message = DS.Model.extend({ 25 | owner: DS.belongsTo('App.Person', {polymorphic: true}) 26 | }); 27 | 28 | App.Post = App.Message.extend({}); 29 | App.Comment = App.Message.extend({}); 30 | 31 | 32 | App.User.FIXTURES = [{ 33 | id: "1", 34 | name: "Alice", 35 | messages: [ 36 | {id: "1", type: "post"}, 37 | {id: "2", type: "comment"} 38 | ] 39 | }]; 40 | 41 | App.Admin.FIXTURES = [{ 42 | id: "2", 43 | name: "Bob", 44 | messages: [{id: "3", type: "post"}] 45 | }]; 46 | 47 | App.Post.FIXTURES = [{ 48 | id: "1", 49 | owner: "1", 50 | owner_type: "user" 51 | }, { 52 | id: "3", 53 | owner: "2", 54 | owner_type: "admin" 55 | }]; 56 | 57 | App.Comment.FIXTURES = [{ 58 | id: "2", 59 | owner: "1", 60 | owner_type: "user" 61 | }]; 62 | 63 | DS.FixtureAdapter.configure(App.User, { alias: 'user' }); 64 | DS.FixtureAdapter.configure(App.Admin, { alias: 'admin' }); 65 | DS.FixtureAdapter.configure(App.Post, { alias: 'post' }); 66 | DS.FixtureAdapter.configure(App.Comment, { alias: 'comment' }); 67 | 68 | adapter = DS.FixtureAdapter.create({ 69 | simulateRemoteResponse: false 70 | }); 71 | 72 | store = DS.Store.create({ adapter: adapter }); 73 | }, 74 | 75 | teardown: function() { 76 | adapter.destroy(); 77 | store.destroy(); 78 | } 79 | }); 80 | 81 | test("records are persisted as is", function() { 82 | var attributes = { 83 | name: "Adam Hawkins", 84 | profile: ComplexObject.create({ 85 | skills: ['ruby', 'javascript'], 86 | music: 'Trance' 87 | }) 88 | }; 89 | 90 | var record = store.createRecord(App.Person, attributes); 91 | store.commit(); 92 | 93 | var adam = store.find(App.Person, record.get('id')); 94 | 95 | equal(adam.get('name'), attributes.name, 'Attribute materialized'); 96 | equal(adam.get('profile'), attributes.profile, 'Complex object materialized'); 97 | 98 | var fixtures = adapter.fixturesForType(App.Person); 99 | equal(fixtures.length, 1, "fixtures updated"); 100 | 101 | var inMemoryProfile = fixtures[0].profile; 102 | ok(inMemoryProfile instanceof Ember.Object, 'Complex objects persisted in memory'); 103 | equal(inMemoryProfile.skills, adam.get('profile.skills')); 104 | equal(inMemoryProfile.music, adam.get('profile.music')); 105 | }); 106 | 107 | test("records are updated as is", function() { 108 | var attributes = { 109 | name: "Adam Hawkins", 110 | profile: ComplexObject.create({ 111 | skills: ['ruby', 'javascript'], 112 | music: 'Trance' 113 | }) 114 | }; 115 | 116 | var record = store.createRecord(App.Person, attributes); 117 | store.commit(); 118 | 119 | var adam = store.find(App.Person, record.get('id')); 120 | 121 | adam.set('name', 'Adam Andrew Hawkins'); 122 | store.commit(); 123 | 124 | equal(adam.get('name'), 'Adam Andrew Hawkins', 'Attribute materialized'); 125 | 126 | var fixtures = adapter.fixturesForType(App.Person); 127 | equal(fixtures.length, 1, "fixtures updated"); 128 | 129 | var inMemoryObject = fixtures[0]; 130 | 131 | equal(inMemoryObject.name, adam.get('name'), 'Changes saved to in memory records'); 132 | }); 133 | 134 | test("records are deleted", function() { 135 | var attributes = { 136 | name: "Adam Hawkins", 137 | profile: ComplexObject.create({ 138 | skills: ['ruby', 'javascript'], 139 | music: 'Trance' 140 | }) 141 | }; 142 | 143 | var record = store.createRecord(App.Person, attributes); 144 | store.commit(); 145 | 146 | var adam = store.find(App.Person, record.get('id')); 147 | adam.deleteRecord(); 148 | store.commit(); 149 | 150 | var fixtures = adapter.fixturesForType(App.Person); 151 | equal(fixtures.length, 0, "fixtures updated"); 152 | }); 153 | 154 | test("find queries loaded records", function() { 155 | var attributes = { 156 | id: '1', 157 | name: "Adam Hawkins", 158 | profile: ComplexObject.create({ 159 | skills: ['ruby', 'javascript'], 160 | music: 'Trance' 161 | }) 162 | }; 163 | 164 | adapter.updateFixtures(App.Person, attributes); 165 | 166 | var adam = store.find(App.Person, 1); 167 | 168 | equal(adam.get('name'), attributes.name, 'Attribute materialized'); 169 | equal(adam.get('profile'), attributes.profile, 'Complex object materialized'); 170 | }); 171 | 172 | test("polymorphic has many", function () { 173 | var alice, bob; 174 | 175 | Ember.run(function () { 176 | alice = store.find(App.User, 1); 177 | }); 178 | 179 | equal(alice.get('name'), "Alice", 'record materialized'); 180 | equal(alice.get('messages.length'), 2, 'correct number of messages'); 181 | equal(alice.get('messages').objectAt(0).constructor, App.Post, 'correct message subclass'); 182 | equal(alice.get('messages').objectAt(0).get('id'), "1", 'correct record'); 183 | equal(alice.get('messages').objectAt(1).constructor, App.Comment, 'correct message subclass'); 184 | equal(alice.get('messages').objectAt(1).get('id'), "2", 'correct record'); 185 | 186 | Ember.run(function () { 187 | bob = store.find(App.Admin, 2); 188 | }); 189 | 190 | equal(bob.get('name'), "Bob", 'record materialized'); 191 | equal(bob.get('messages.length'), 1, 'correct number of messages'); 192 | equal(bob.get('messages').objectAt(0).constructor, App.Post, 'correct message subclass'); 193 | equal(bob.get('messages').objectAt(0).get('id'), "3", 'correct record'); 194 | }); 195 | 196 | test("polymorphic belongs to", function () { 197 | var alice_post, bob_post, alice, bob; 198 | 199 | Ember.run(function () { 200 | alice_post = store.find(App.Post, 1); 201 | bob_post = store.find(App.Post, 3); 202 | }); 203 | 204 | Ember.run(function () { 205 | alice = alice_post.get('owner'); 206 | bob = bob_post.get('owner'); 207 | }); 208 | 209 | equal(alice.get('name'), "Alice", 'correct owner'); 210 | equal(alice.constructor, App.User, 'correct person subclass'); 211 | equal(bob.get('name'), "Bob", 'correct owner'); 212 | equal(bob.constructor, App.Admin, 'correct person subclass'); 213 | }); -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/transactions/relationships_test.js: -------------------------------------------------------------------------------- 1 | /*global QUnit*/ 2 | 3 | var Post = DS.Model.extend({ 4 | title: DS.attr('string'), 5 | body: DS.attr('string') 6 | }); 7 | 8 | var Comment = DS.Model.extend({ 9 | body: DS.attr('string'), 10 | post: DS.belongsTo(Post) 11 | }); 12 | 13 | Post.reopen({ 14 | comments: DS.hasMany(Comment) 15 | }); 16 | 17 | var store, adapter, transaction; 18 | 19 | module("Transactions and Relationships", { 20 | setup: function() { 21 | adapter = DS.Adapter.create(); 22 | store = DS.Store.create({ 23 | adapter: adapter 24 | }); 25 | }, 26 | 27 | teardown: function() { 28 | if (transaction) { transaction.destroy(); } 29 | adapter.destroy(); 30 | store.destroy(); 31 | } 32 | }); 33 | 34 | function expectRelationships(description) { 35 | var relationships = transaction.get('relationships').toArray(), 36 | relationship = relationships[0], 37 | count = description.count === undefined ? description.length : description.count; 38 | 39 | if(description.count === undefined && (!description[0] || !description[1])){ 40 | count = 1; 41 | } 42 | QUnit.push(relationships.length === count, relationships.length, count, "There should be " + count + " dirty relationships"); 43 | 44 | if (count) { 45 | if(description[0]){ 46 | QUnit.push(relationships[0].getSecondRecord() === description[0].parent, relationships[0].getSecondRecord(), description[0].parent, "oldParent is incorrect"); 47 | QUnit.push(relationships[0].getFirstRecord() === description[0].child, relationships[0].child, description[0].child, "child in relationship 0 is incorrect"); 48 | } 49 | if(description[1]){ 50 | var relPosition = count === 2 ? 1 : 0; 51 | QUnit.push(relationships[relPosition].getFirstRecord() === description[1].child, relationships[relPosition].child, description[1].child, "child in relationship 1 is incorrect"); 52 | QUnit.push(relationships[relPosition].getSecondRecord() === description[1].parent, relationships[relPosition].parent, description[1].parent, "newParent is incorrect"); 53 | } 54 | } 55 | } 56 | 57 | test("If both the parent and child are clean and in the same transaction, a dirty relationship is added to the transaction null->A", function() { 58 | store.load(Post, { id: 1, title: "Ohai", body: "FIRST POST ZOMG" }); 59 | store.load(Comment, { id: 1, body: "Kthx" }); 60 | 61 | var post = store.find(Post, 1); 62 | var comment = store.find(Comment, 1); 63 | 64 | transaction = store.transaction(); 65 | 66 | transaction.add(post); 67 | transaction.add(comment); 68 | 69 | post.get('comments').pushObject(comment); 70 | 71 | expectRelationships( 72 | [null,{parent: post, child: comment}] 73 | ); 74 | }); 75 | 76 | test("If a child is removed from a parent, a dirty relationship is added to the transaction A->null", function() { 77 | store.load(Comment, { id: 1, body: "Kthx" }); 78 | store.load(Post, { id: 1, title: "Ohai", body: "FIRST POST ZOMG", comments: [ 1 ] }); 79 | 80 | var post = store.find(Post, 1); 81 | var comment = store.find(Comment, 1); 82 | 83 | transaction = store.transaction(); 84 | 85 | transaction.add(post); 86 | transaction.add(comment); 87 | 88 | post.get('comments').removeObject(comment); 89 | 90 | expectRelationships( 91 | [{parent: post, 92 | child: comment}] 93 | ); 94 | }); 95 | 96 | test("If a child is removed from a parent it was recently added to, the dirty relationship is removed. null->A, A->null", function() { 97 | store.load(Comment, { id: 1, body: "Kthx" }); 98 | store.load(Post, { id: 1, title: "Ohai", body: "FIRST POST ZOMG", comments: [ 1 ] }); 99 | 100 | var post = store.find(Post, 1); 101 | var comment = store.find(Comment, 1); 102 | 103 | transaction = store.transaction(); 104 | 105 | transaction.add(post); 106 | transaction.add(comment); 107 | 108 | post.get('comments').removeObject(comment); 109 | post.get('comments').pushObject(comment); 110 | 111 | expectRelationships({ count: 0 }); 112 | }); 113 | 114 | test("If a child was added to one parent, and then another, the changes coalesce. A->B, B->C", function() { 115 | store.load(Comment, { id: 1, body: "Kthx" }); 116 | store.load(Post, { id: 1, title: "Ohai", body: "FIRST POST ZOMG", comments: [ 1 ] }); 117 | store.load(Post, { id: 2, title: "ZOMG", body: "SECOND POST WAT" }); 118 | store.load(Post, { id: 3, title: "ORLY?", body: "Why am I still here?" }); 119 | 120 | var post = store.find(Post, 1); 121 | var post2 = store.find(Post, 2); 122 | var post3 = store.find(Post, 3); 123 | var comment = store.find(Comment, 1); 124 | 125 | transaction = store.transaction(); 126 | 127 | transaction.add(post); 128 | transaction.add(comment); 129 | 130 | post.get('comments').removeObject(comment); 131 | post2.get('comments').pushObject(comment); 132 | post2.get('comments').removeObject(comment); 133 | post3.get('comments').pushObject(comment); 134 | 135 | expectRelationships([{parent:post, child:comment},{parent:post3, child:comment}]); 136 | }); 137 | 138 | test("the store should have a new defaultTransaction after commit from store", function() { 139 | store.load(Post, { id: 1, title: "Ohai" }); 140 | 141 | var record = store.find(Post, 1); 142 | var transaction = record.get('transaction'); 143 | var defaultTransaction = store.get('defaultTransaction'); 144 | 145 | equal(transaction, defaultTransaction, 'record is in the defaultTransaction'); 146 | 147 | store.commit(); 148 | 149 | var newDefaultTransaction = store.get('defaultTransaction'); 150 | transaction = record.get('transaction'); 151 | 152 | ok(defaultTransaction !== newDefaultTransaction, "store should have a new defaultTransaction"); 153 | equal(transaction, newDefaultTransaction, 'record is in the new defaultTransaction'); 154 | }); 155 | 156 | test("the store should have a new defaultTransaction after commit from defaultTransaction", function() { 157 | store.load(Post, { id: 1, title: "Ohai" }); 158 | 159 | var record = store.find(Post, 1); 160 | var transaction = record.get('transaction'); 161 | var defaultTransaction = store.get('defaultTransaction'); 162 | 163 | equal(transaction, defaultTransaction, 'record is in the defaultTransaction'); 164 | 165 | defaultTransaction.commit(); 166 | 167 | var newDefaultTransaction = store.get('defaultTransaction'); 168 | transaction = record.get('transaction'); 169 | 170 | ok(defaultTransaction !== newDefaultTransaction, "store should have a new defaultTransaction"); 171 | equal(transaction, newDefaultTransaction, 'record is in the new defaultTransaction'); 172 | }); 173 | 174 | test("the store should have a new defaultTransaction after commit from record's transaction", function() { 175 | store.load(Post, { id: 1, title: "Ohai" }); 176 | 177 | var record = store.find(Post, 1); 178 | var transaction = record.get('transaction'); 179 | var defaultTransaction = store.get('defaultTransaction'); 180 | 181 | equal(transaction, defaultTransaction, 'record is in the defaultTransaction'); 182 | 183 | transaction.commit(); 184 | 185 | var newDefaultTransaction = store.get('defaultTransaction'); 186 | transaction = record.get('transaction'); 187 | 188 | ok(defaultTransaction !== newDefaultTransaction, "store should have a new defaultTransaction"); 189 | equal(transaction, newDefaultTransaction, 'record is in the new defaultTransaction'); 190 | }); 191 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/one_to_one_relationships_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("One-to-One Relationships", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | 18 | App.Post = DS.Model.extend({ 19 | title: DS.attr('string') 20 | }); 21 | 22 | App.Comment = DS.Model.extend({ 23 | body: DS.attr('string'), 24 | post: DS.belongsTo(App.Post) 25 | }); 26 | 27 | App.Post.reopen({ 28 | comment: DS.belongsTo(App.Comment) 29 | }); 30 | }, 31 | 32 | teardown: function() { 33 | Ember.run(function() { 34 | store.destroy(); 35 | }); 36 | } 37 | }); 38 | 39 | function verifySynchronizedOneToOne(post, comment, expectedHasMany) { 40 | equal(comment.get('post'), post); 41 | equal(post.get('comment'), comment); 42 | } 43 | 44 | test("When setting a record's belongsTo relationship to another record, that record should be added to the inverse belongsTo", function() { 45 | store.load(App.Post, { id: 1, title: "parent" }); 46 | store.load(App.Comment, { id: 2, body: "child" }); 47 | 48 | var post = store.find(App.Post, 1), 49 | comment = store.find(App.Comment, 2); 50 | 51 | comment.set('post', post); 52 | verifySynchronizedOneToOne(post, comment); 53 | }); 54 | /* 55 | test("When setting a record's belongsTo relationship to null, that record should be removed from the inverse hasMany array", function() { 56 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 57 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 58 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 59 | 60 | var post = store.find(App.Post, 1), 61 | comment1 = store.find(App.Comment, 2), 62 | comment2 = store.find(App.Comment, 3); 63 | 64 | deepEqual(post.get('comments').toArray(), [comment1, comment2], "precond - the post has has two child comments"); 65 | 66 | comment1.set('post', null); 67 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 68 | deepEqual(post.get('comments').toArray(), [ comment2 ], "the post comments array should have the remaining comment"); 69 | }); 70 | 71 | test("When adding a record to a hasMany array, its belongsTo is set", function() { 72 | store.load(App.Post, { id: 1, title: "parent", comments: [2] }); 73 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 74 | store.load(App.Comment, { id: 3, body: "child" }); 75 | 76 | var post = store.find(App.Post, 1), 77 | comment1 = store.find(App.Comment, 2), 78 | comment2 = store.find(App.Comment, 3); 79 | 80 | post.get('comments').addObject(comment2); 81 | verifySynchronizedOneToMany(post, comment2, [comment1, comment2]); 82 | }); 83 | 84 | test("When removing a record from a hasMany array, its belongsTo is set to null", function() { 85 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 86 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 87 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 88 | 89 | var post = store.find(App.Post, 1), 90 | comment1 = store.find(App.Comment, 2), 91 | comment2 = store.find(App.Comment, 3); 92 | 93 | post.get('comments').removeObject(comment1); 94 | verifySynchronizedOneToMany(post, comment2); 95 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 96 | }); 97 | 98 | test("When adding a record to a hasMany array, it should be removed from its old hasMany array, if there was one", function() { 99 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 100 | store.load(App.Post, { id: 2, title: "new parent" }); 101 | 102 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 103 | 104 | var oldParent = store.find(App.Post, 1), 105 | newParent = store.find(App.Post, 2), 106 | child = store.find(App.Comment, 3); 107 | 108 | verifySynchronizedOneToMany(oldParent, child); 109 | 110 | newParent.get('comments').addObject(child); 111 | 112 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 113 | 114 | verifySynchronizedOneToMany(newParent, child); 115 | }); 116 | 117 | test("When changing a record's belongsTo, it should be removed from its old inverse hasMany array, if there was one", function() { 118 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 119 | store.load(App.Post, { id: 2, title: "new parent" }); 120 | 121 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 122 | 123 | var oldParent = store.find(App.Post, 1), 124 | newParent = store.find(App.Post, 2), 125 | child = store.find(App.Comment, 3); 126 | 127 | verifySynchronizedOneToMany(oldParent, child); 128 | 129 | child.set('post', newParent); 130 | 131 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 132 | 133 | verifySynchronizedOneToMany(newParent, child); 134 | }); 135 | 136 | test("Deleting a record removes it from any inverse hasMany arrays to which it belongs.", function() { 137 | var post, comment; 138 | 139 | store.load(App.Post, { id: 1, title: "parent", comments: [1] }); 140 | store.load(App.Comment, { id: 1, title: "parent", post: 1 }); 141 | 142 | post = store.find(App.Post, 1); 143 | comment = store.find(App.Comment, 1); 144 | 145 | verifySynchronizedOneToMany(post, comment); 146 | 147 | comment.deleteRecord(); 148 | 149 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 150 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 151 | }); 152 | 153 | test("Deleting a newly created record removes it from any inverse hasMany arrays to which it belongs.", function() { 154 | var post, comment; 155 | 156 | store.load(App.Post, { id: 1, title: "parent" }); 157 | 158 | post = store.find(App.Post, 1); 159 | comment = store.createRecord(App.Comment); 160 | 161 | equal(comment.get('post'), null, "precond - the child should not yet belong to anyone"); 162 | 163 | post.get('comments').addObject(comment); 164 | 165 | verifySynchronizedOneToMany(post, comment); 166 | 167 | comment.deleteRecord(); 168 | 169 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 170 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 171 | }); 172 | 173 | //test("When a record with a hasMany association is deleted, its associated record is materialized and its belongsTo is changed", function() { 174 | //store.load(App.Post, { id: 1, title: "NEW! Ember Table", comments: [ 2 ] }); 175 | //store.load(App.Comment, { id: 2, body: "Needs more async", post: 1 }); 176 | 177 | //// Only find the post, not the comment. This ensures 178 | //// that the comment is not yet materialized. 179 | //var post = store.find(App.Post, 1); 180 | //var comment = store.find(App.Comment, 2); 181 | //post.deleteRecord(); 182 | 183 | //// Now that we've deleted the post, we should materialize the 184 | //// comment and ensure that its inverse relationship has been 185 | //// modified appropriately (i.e., set to null) 186 | //equal(comment.get('post'), null, "the comment's post belongsTo relationship was set to null"); 187 | //}); 188 | */ 189 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/many_to_many_relationships_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("One-to-Many Relationships", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | 18 | App.Post = DS.Model.extend({ 19 | title: DS.attr('string') 20 | }); 21 | 22 | App.Comment = DS.Model.extend({ 23 | body: DS.attr('string'), 24 | posts: DS.hasMany(App.Post) 25 | }); 26 | 27 | App.Post.reopen({ 28 | comments: DS.hasMany(App.Comment) 29 | }); 30 | }, 31 | 32 | teardown: function() { 33 | Ember.run(function() { 34 | store.destroy(); 35 | }); 36 | } 37 | }); 38 | 39 | function verifySynchronizedManyToMany(post, comment, expectedHasMany) { 40 | expectedHasMany = expectedHasMany || [comment]; 41 | deepEqual(post.get('comments').toArray(), [comment]); 42 | deepEqual(comment.get('posts').toArray(), [post]); 43 | } 44 | 45 | test("When adding another record to a hasMany relationship, that record should be added to the inverse hasMany array", function() { 46 | store.load(App.Post, { id: 1, title: "parent" }); 47 | store.load(App.Comment, { id: 2, body: "child" }); 48 | 49 | var post = store.find(App.Post, 1), 50 | comment = store.find(App.Comment, 2); 51 | 52 | equal(post.get('comments.length'), 0, "precond - the post has no child comments yet"); 53 | 54 | comment.get('posts').addObject(post); 55 | verifySynchronizedManyToMany(post, comment); 56 | }); 57 | /* 58 | test("When setting a record's belongsTo relationship to null, that record should be removed from the inverse hasMany array", function() { 59 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 60 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 61 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 62 | 63 | var post = store.find(App.Post, 1), 64 | comment1 = store.find(App.Comment, 2), 65 | comment2 = store.find(App.Comment, 3); 66 | 67 | deepEqual(post.get('comments').toArray(), [comment1, comment2], "precond - the post has has two child comments"); 68 | 69 | comment1.set('post', null); 70 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 71 | deepEqual(post.get('comments').toArray(), [ comment2 ], "the post comments array should have the remaining comment"); 72 | }); 73 | 74 | test("When adding a record to a hasMany array, its belongsTo is set", function() { 75 | store.load(App.Post, { id: 1, title: "parent", comments: [2] }); 76 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 77 | store.load(App.Comment, { id: 3, body: "child" }); 78 | 79 | var post = store.find(App.Post, 1), 80 | comment1 = store.find(App.Comment, 2), 81 | comment2 = store.find(App.Comment, 3); 82 | 83 | post.get('comments').addObject(comment2); 84 | verifySynchronizedOneToMany(post, comment2, [comment1, comment2]); 85 | }); 86 | 87 | test("When removing a record from a hasMany array, its belongsTo is set to null", function() { 88 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 89 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 90 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 91 | 92 | var post = store.find(App.Post, 1), 93 | comment1 = store.find(App.Comment, 2), 94 | comment2 = store.find(App.Comment, 3); 95 | 96 | post.get('comments').removeObject(comment1); 97 | verifySynchronizedOneToMany(post, comment2); 98 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 99 | }); 100 | 101 | test("When adding a record to a hasMany array, it should be removed from its old hasMany array, if there was one", function() { 102 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 103 | store.load(App.Post, { id: 2, title: "new parent" }); 104 | 105 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 106 | 107 | var oldParent = store.find(App.Post, 1), 108 | newParent = store.find(App.Post, 2), 109 | child = store.find(App.Comment, 3); 110 | 111 | verifySynchronizedOneToMany(oldParent, child); 112 | 113 | newParent.get('comments').addObject(child); 114 | 115 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 116 | 117 | verifySynchronizedOneToMany(newParent, child); 118 | }); 119 | 120 | test("When changing a record's belongsTo, it should be removed from its old inverse hasMany array, if there was one", function() { 121 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 122 | store.load(App.Post, { id: 2, title: "new parent" }); 123 | 124 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 125 | 126 | var oldParent = store.find(App.Post, 1), 127 | newParent = store.find(App.Post, 2), 128 | child = store.find(App.Comment, 3); 129 | 130 | verifySynchronizedOneToMany(oldParent, child); 131 | 132 | child.set('post', newParent); 133 | 134 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 135 | 136 | verifySynchronizedOneToMany(newParent, child); 137 | }); 138 | 139 | test("Deleting a record removes it from any inverse hasMany arrays to which it belongs.", function() { 140 | var post, comment; 141 | 142 | store.load(App.Post, { id: 1, title: "parent", comments: [1] }); 143 | store.load(App.Comment, { id: 1, title: "parent", post: 1 }); 144 | 145 | post = store.find(App.Post, 1); 146 | comment = store.find(App.Comment, 1); 147 | 148 | verifySynchronizedOneToMany(post, comment); 149 | 150 | comment.deleteRecord(); 151 | 152 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 153 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 154 | }); 155 | 156 | test("Deleting a newly created record removes it from any inverse hasMany arrays to which it belongs.", function() { 157 | var post, comment; 158 | 159 | store.load(App.Post, { id: 1, title: "parent" }); 160 | 161 | post = store.find(App.Post, 1); 162 | comment = store.createRecord(App.Comment); 163 | 164 | equal(comment.get('post'), null, "precond - the child should not yet belong to anyone"); 165 | 166 | post.get('comments').addObject(comment); 167 | 168 | verifySynchronizedOneToMany(post, comment); 169 | 170 | comment.deleteRecord(); 171 | 172 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 173 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 174 | }); 175 | 176 | //test("When a record with a hasMany association is deleted, its associated record is materialized and its belongsTo is changed", function() { 177 | //store.load(App.Post, { id: 1, title: "NEW! Ember Table", comments: [ 2 ] }); 178 | //store.load(App.Comment, { id: 2, body: "Needs more async", post: 1 }); 179 | 180 | //// Only find the post, not the comment. This ensures 181 | //// that the comment is not yet materialized. 182 | //var post = store.find(App.Post, 1); 183 | //var comment = store.find(App.Comment, 2); 184 | //post.deleteRecord(); 185 | 186 | //// Now that we've deleted the post, we should materialize the 187 | //// comment and ensure that its inverse relationship has been 188 | //// modified appropriately (i.e., set to null) 189 | //equal(comment.get('post'), null, "the comment's post belongsTo relationship was set to null"); 190 | //}); 191 | */ 192 | -------------------------------------------------------------------------------- /packages/ember-data/tests/integration/relationships/one_to_many_relationships_test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | 3 | var store, adapter, App, Post, Comment; 4 | 5 | module("One-to-Many Relationships", { 6 | setup: function() { 7 | adapter = DS.Adapter.create(); 8 | 9 | store = DS.Store.create({ 10 | isDefaultStore: true, 11 | adapter: adapter 12 | }); 13 | 14 | App = Ember.Namespace.create({ 15 | toString: function() { return "App"; } 16 | }); 17 | 18 | App.Post = DS.Model.extend({ 19 | title: DS.attr('string') 20 | }); 21 | 22 | App.Comment = DS.Model.extend({ 23 | body: DS.attr('string'), 24 | post: DS.belongsTo(App.Post) 25 | }); 26 | 27 | App.Post.reopen({ 28 | comments: DS.hasMany(App.Comment) 29 | }); 30 | }, 31 | 32 | teardown: function() { 33 | Ember.run(function() { 34 | store.destroy(); 35 | }); 36 | } 37 | }); 38 | 39 | function verifySynchronizedOneToMany(post, comment, expectedHasMany) { 40 | expectedHasMany = expectedHasMany || [comment]; 41 | equal(comment.get('post'), post); 42 | deepEqual(post.get('comments').toArray(), expectedHasMany); 43 | } 44 | 45 | test("Referencing a null belongsTo relationship returns null", function(){ 46 | store.load(App.Comment, { id: 1, post: null, body: "child with intentionally null parent" }); 47 | var comment = store.find(App.Comment, 1); 48 | equal(comment.get('post'), null, "null belongsTo relationship returns null"); 49 | }); 50 | 51 | test("When setting a record's belongsTo relationship to another record, that record should be added to the inverse hasMany array", function() { 52 | store.load(App.Post, { id: 1, title: "parent" }); 53 | store.load(App.Comment, { id: 2, body: "child" }); 54 | 55 | var post = store.find(App.Post, 1), 56 | comment = store.find(App.Comment, 2); 57 | 58 | equal(post.get('comments.length'), 0, "precond - the post has no child comments yet"); 59 | 60 | comment.set('post', post); 61 | verifySynchronizedOneToMany(post, comment); 62 | }); 63 | 64 | test("When setting a record's belongsTo relationship to null, that record should be removed from the inverse hasMany array", function() { 65 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 66 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 67 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 68 | 69 | var post = store.find(App.Post, 1), 70 | comment1 = store.find(App.Comment, 2), 71 | comment2 = store.find(App.Comment, 3); 72 | 73 | deepEqual(post.get('comments').toArray(), [comment1, comment2], "precond - the post has has two child comments"); 74 | 75 | comment1.set('post', null); 76 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 77 | deepEqual(post.get('comments').toArray(), [ comment2 ], "the post comments array should have the remaining comment"); 78 | }); 79 | 80 | test("When adding a record to a hasMany array, its belongsTo is set", function() { 81 | store.load(App.Post, { id: 1, title: "parent", comments: [2] }); 82 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 83 | store.load(App.Comment, { id: 3, body: "child" }); 84 | 85 | var post = store.find(App.Post, 1), 86 | comment1 = store.find(App.Comment, 2), 87 | comment2 = store.find(App.Comment, 3); 88 | 89 | post.get('comments').addObject(comment2); 90 | verifySynchronizedOneToMany(post, comment2, [comment1, comment2]); 91 | }); 92 | 93 | test("When removing a record from a hasMany array, its belongsTo is set to null", function() { 94 | store.load(App.Post, { id: 1, title: "parent", comments: [2, 3] }); 95 | store.load(App.Comment, { id: 2, body: "child", post: 1 }); 96 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 97 | 98 | var post = store.find(App.Post, 1), 99 | comment1 = store.find(App.Comment, 2), 100 | comment2 = store.find(App.Comment, 3); 101 | 102 | post.get('comments').removeObject(comment1); 103 | verifySynchronizedOneToMany(post, comment2); 104 | equal(comment1.get('post'), null, "belongsTo relationship has been set to null"); 105 | }); 106 | 107 | test("When adding a record to a hasMany array, it should be removed from its old hasMany array, if there was one", function() { 108 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 109 | store.load(App.Post, { id: 2, title: "new parent" }); 110 | 111 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 112 | 113 | var oldParent = store.find(App.Post, 1), 114 | newParent = store.find(App.Post, 2), 115 | child = store.find(App.Comment, 3); 116 | 117 | verifySynchronizedOneToMany(oldParent, child); 118 | 119 | newParent.get('comments').addObject(child); 120 | 121 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 122 | 123 | verifySynchronizedOneToMany(newParent, child); 124 | }); 125 | 126 | test("When changing a record's belongsTo, it should be removed from its old inverse hasMany array, if there was one", function() { 127 | store.load(App.Post, { id: 1, title: "old parent", comments: [3] }); 128 | store.load(App.Post, { id: 2, title: "new parent" }); 129 | 130 | store.load(App.Comment, { id: 3, body: "child", post: 1 }); 131 | 132 | var oldParent = store.find(App.Post, 1), 133 | newParent = store.find(App.Post, 2), 134 | child = store.find(App.Comment, 3); 135 | 136 | verifySynchronizedOneToMany(oldParent, child); 137 | 138 | child.set('post', newParent); 139 | 140 | deepEqual(oldParent.get('comments').toArray(), [], "old parent has no child comments"); 141 | 142 | verifySynchronizedOneToMany(newParent, child); 143 | }); 144 | 145 | test("Deleting a record removes it from any inverse hasMany arrays to which it belongs.", function() { 146 | var post, comment; 147 | 148 | store.load(App.Post, { id: 1, title: "parent", comments: [1] }); 149 | store.load(App.Comment, { id: 1, title: "parent", post: 1 }); 150 | 151 | post = store.find(App.Post, 1); 152 | comment = store.find(App.Comment, 1); 153 | 154 | verifySynchronizedOneToMany(post, comment); 155 | 156 | comment.deleteRecord(); 157 | 158 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 159 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 160 | }); 161 | 162 | test("Deleting a newly created record removes it from any inverse hasMany arrays to which it belongs.", function() { 163 | var post, comment; 164 | 165 | store.load(App.Post, { id: 1, title: "parent" }); 166 | 167 | post = store.find(App.Post, 1); 168 | comment = store.createRecord(App.Comment); 169 | 170 | equal(comment.get('post'), null, "precond - the child should not yet belong to anyone"); 171 | 172 | post.get('comments').addObject(comment); 173 | 174 | verifySynchronizedOneToMany(post, comment); 175 | 176 | comment.deleteRecord(); 177 | 178 | equal(comment.get('post'), null, "the comment should no longer belong to a post"); 179 | deepEqual(post.get('comments').toArray(), [], "the post should no longer have any comments"); 180 | }); 181 | 182 | //test("When a record with a hasMany relationship is deleted, its associated record is materialized and its belongsTo is changed", function() { 183 | //store.load(App.Post, { id: 1, title: "NEW! Ember Table", comments: [ 2 ] }); 184 | //store.load(App.Comment, { id: 2, body: "Needs more async", post: 1 }); 185 | 186 | //// Only find the post, not the comment. This ensures 187 | //// that the comment is not yet materialized. 188 | //var post = store.find(App.Post, 1); 189 | //var comment = store.find(App.Comment, 2); 190 | //post.deleteRecord(); 191 | 192 | //// Now that we've deleted the post, we should materialize the 193 | //// comment and ensure that its inverse relationship has been 194 | //// modified appropriately (i.e., set to null) 195 | //equal(comment.get('post'), null, "the comment's post belongsTo relationship was set to null"); 196 | //}); 197 | --------------------------------------------------------------------------------