<|start_filename|>src/attribute_type/object.js<|end_filename|>
import Ember from 'ember';
import EmberGraphSet from 'ember-graph/util/set';
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * @class ObjectType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * If the value is a JSON object, it's returned.
	 * Otherwise, it serializes to `null`.
	 *
	 * @method serialize
	 * @param {Object} obj
	 * @return {Object}
	 */
	serialize: function(obj) {
		if (this.isObject(obj)) {
			try {
				JSON.stringify(obj);
				return obj;
			} catch (e) {
				return null;
			}
		} else {
			return null;
		}
	},

	/**
	 * Returns the value if it's an object, `null` otherwise.
	 *
	 * @method deserialize
	 * @param {Object} json
	 * @return {Object}
	 */
	deserialize: function(json) {
		if (this.isObject(json)) {
			return json;
		} else {
			return null;
		}
	},

	/**
	 * Checks for equality using
	 * {{link-to-method 'ObjectType' 'deepCompare'}}.
	 *
	 * @method isEqual
	 * @param {Object} a
	 * @param {Object} b
	 * @return {Boolean}
	 */
	isEqual: function(a, b) {
		if (!this.isObject(a) || !this.isObject(b)) {
			return false;
		}

		return this.deepCompare(a, b);
	},

	/**
	 * Determines if the value is a plain Javascript object.
	 *
	 * @method isObject
	 * @param {Object} obj
	 * @return {Boolean}
	 */
	isObject: function(obj) {
		return !Ember.isNone(obj) && Ember.typeOf(obj) === 'object' && obj.constructor === Object;
	},

	/**
	 * Performs a deep comparison on the objects, iterating
	 * objects and arrays, and using `===` on primitives.
	 *
	 * @method deepCompare
	 * @param {Object} a
	 * @param {Object} b
	 * @return {Boolean}
	 */
	deepCompare: function(a, b) {
		if (this.isObject(a) && this.isObject(b)) {
			var aKeys = EmberGraphSet.create();
			var bKeys = EmberGraphSet.create();

			aKeys.addObjects(Object.keys(a));
			bKeys.addObjects(Object.keys(b));

			if (!aKeys.isEqual(bKeys)) {
				return false;
			}

			var keys = Object.keys(a);

			for (var i = 0; i < keys.length; i = i + 1) {
				if (!this.deepCompare(a[keys[i]], b[keys[i]])) {
					return false;
				}
			}

			return true;
		} else if (Ember.isArray(a) && Ember.isArray(b)) {
			return Ember.compare(a, b) === 0;
		} else {
			return (a === b);
		}
	}
});

<|start_filename|>src/attribute_type/boolean.js<|end_filename|>
import Ember from 'ember';
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * @class BooleanType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * @property defaultValue
	 * @default false
	 * @final
	 */
	defaultValue: false,

	/**
	 * Coerces to a boolean using
	 * {{link-to-method 'BooleanType' 'coerceToBoolean'}}.
	 *
	 * @method serialize
	 * @param {Boolean} bool
	 * @return {Boolean}
	 */
	serialize: function(bool) {
		return this.coerceToBoolean(bool);
	},

	/**
	 * Coerces to a boolean using
	 * {{link-to-method 'BooleanType' 'coerceToBoolean'}}.
	 *
	 * @method deserialize
	 * @param {Boolean} json
	 * @return {Boolean}
	 */
	deserialize: function(json) {
		return this.coerceToBoolean(json);
	},

	/**
	 * Coerces a value to a boolean. `true` and `'true'` resolve to
	 * `true`, everything else resolves to `false`.
	 *
	 * @method coerceToBoolean
	 * @param {Any} obj
	 * @return {Boolean}
	 */
	coerceToBoolean: function(obj) {
		if (Ember.typeOf(obj) === 'boolean' && obj == true) { // eslint-disable-line eqeqeq
			return true;
		}

		if (Ember.typeOf(obj) === 'string' && obj == 'true') { // eslint-disable-line eqeqeq
			return true;
		}

		return false;
	}
});

<|start_filename|>src/attribute_type/string.js<|end_filename|>
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * @class StringType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * Coerces the given value to a string, unless it's `null`,
	 * in which case it returns `null`.
	 *
	 * @method serialize
	 * @param {String} str
	 * @returns {String}
	 */
	serialize: function(str) {
		return (str === null || str === undefined ? null : '' + str);
	},

	/**
	 * Coerces the given value to a string, unless it's `null`,
	 * in which case it returns `null`.
	 *
	 * @method deserialize
	 * @param {String} json
	 * @returns {String}
	 */
	deserialize: function(json) {
		return (json === null || json === undefined ? null : '' + json);
	}
});

<|start_filename|>src/constants.js<|end_filename|>
export var RelationshipTypes = {
	HAS_ONE_KEY: 'hasOne',
	HAS_MANY_KEY: 'hasMany'
};

export var RelationshipStates = {
	CLIENT_STATE: 'client',
	SERVER_STATE: 'server',
	DELETED_STATE: 'deleted'
};

<|start_filename|>src/adapter/local_storage.js<|end_filename|>
import Ember from 'ember';
import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter';

const Promise = Ember.RSVP.Promise;

/**
 * This adapter will store all of your application data in the browser's
 * localStorage. This adapter can be useful for caching data on the client,
 * or for testing purposes. If you want to initialize the localStorage
 * with an initial data set, override the
 * {{link-to-method 'LocalStorageAdapter' 'shouldInitializeDatabase'}} and
 * {{link-to-method 'LocalStorageAdapter' 'getInitialPayload'}} hooks.
 *
 * To customize the the behavior for getting or saving records, you can
 * override any of the following methods:
 * {{link-to-method 'LocalStorageAdapter' 'serverFindRecord'}},
 * {{link-to-method 'LocalStorageAdapter' 'serverFindMany'}},
 * {{link-to-method 'LocalStorageAdapter' 'serverFindAll'}},
 * {{link-to-method 'LocalStorageAdapter' 'serverCreateRecord'}},
 * {{link-to-method 'LocalStorageAdapter' 'serverDeleteRecord'}},
 * {{link-to-method 'LocalStorageAdapter' 'serverUpdateRecord'}}.
 *
 * @class LocalStorageAdapter
 * @extends EmberGraphAdapter
 */
export default EmberGraphAdapter.extend({

	/**
	 * @property localStorageKey
	 * @default 'ember-graph.db'
	 * @final
	 * @protected
	 */
	localStorageKey: 'ember-graph.db',

	getDatabase: function() {
		try {
			var key = this.get('localStorageKey');
			var value = localStorage[key];

			if (value) {
				return Promise.resolve(JSON.parse(value));
			} else {
				return Promise.resolve({ records: {}, relationships: [] });
			}
		} catch (error) {
			return Promise.reject(error);
		}
	},

	setDatabase: function(db) {
		try {
			var key = this.get('localStorageKey');
			localStorage[key] = JSON.stringify(db);
			return Promise.resolve(db);
		} catch (error) {
			return Promise.reject(error);
		}
	},

	/**
	 * Initializes the database (if configured to do so).
	 * This function is called at adapter initialization
	 * (which is probably when it's looked up by the container).
	 *
	 * @method initializeDatabase
	 * @private
	 */
	initializeDatabase: function() {
		this._super();
	},

	initializeDatabaseOnInit: Ember.on('init', function() {
		this.initializeDatabase();
	})

});

<|start_filename|>test/store/unload.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var Promise = Em.RSVP.Promise;

	module('Store Unload Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					email: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: true
					})
				}),

				post: EG.Model.extend({
					title: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					body: EG.attr({
						type: 'string',
						isRequired: false,
						defaultValue: ''
					}),

					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: true
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: false
					})
				}),

				tag: EG.Model.extend({
					name: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					})
				}),

				item: EG.Model.extend({
					name: EG.attr({
						type: 'string'
					})
				})
			}, {
				adapter: EG.Adapter.extend({
					updateRecord: function(record) {
						return Promise.resolve();
					}
				})
			});
		}
	});

	test('Unload record with no relationships', function() {
		expect(3);

		store.pushPayload({
			item: [{ id: '1', name: 'First' }]
		});

		var record = store.getRecord('item', '1');

		strictEqual(record.get('id'), '1');
		strictEqual(record.get('name'), 'First');

		store.unloadRecord(record);

		strictEqual(store.getRecord('item', '1'), null);
	});

	test('Unload record with relationships', function() {
		expect(6);

		store.pushPayload({
			user: [{
				id: '1',
				email: '<EMAIL>',
				posts: [
					{ type: 'post', id: '1' },
					{ type: 'post', id: '2' }
				]
			}],
			post: [
				{ id: '1', title: 'One', author: { type: 'user', id: '1' } },
				{ id: '2', title: 'Two', author: { type: 'user', id: '1' } }
			]
		});

		var user = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');
		var post2 = store.getRecord('post', '2');

		deepEqual(user.get('_posts').mapBy('id').sort(), ['1', '2'].sort());
		deepEqual(post1.get('_author'), { type: 'user', id: '1' });
		deepEqual(post2.get('_author'), { type: 'user', id: '1' });

		store.unloadRecord(user);
		strictEqual(store.getRecord('user', '1'), null);
		deepEqual(post1.get('_author'), { type: 'user', id: '1' });
		deepEqual(post2.get('_author'), { type: 'user', id: '1' });
	});

	test('Unloading dirty record throws error', function() {
		expect(1);

		store.pushPayload({
			item: [{ id: '1', name: 'First' }]
		});

		var item = store.getRecord('item', '1');
		item.set('name', '');

		throws(function() {
			store.unloadRecord(item);
		});
	});

	test('Unload dirty record by telling store to clear changes', function() {
		expect(1);

		store.pushPayload({
			item: [{ id: '1', name: 'First' }]
		});

		var item = store.getRecord('item', '1');
		item.set('name', '');
		store.unloadRecord(item, true);

		strictEqual(store.getRecord('item', '1'), null);
	});
})();


<|start_filename|>src/main.js<|end_filename|>
export * from 'ember-graph/util';
export * from 'ember-graph/model/schema';

<|start_filename|>tasks/build_api_pages.js<|end_filename|>
'use strict';

var fs = require('fs');
var Handlebars = require('handlebars');

var templates = {};

module.exports = function(grunt) {
	grunt.registerTask('build_api_pages', function() {
		compileTemplates();

		var data = JSON.parse(fs.readFileSync('./doc/ember-graph.json', { encoding: 'utf8' }));
		buildNamespacePages(data);
		buildClassPages(data);
	});
};

function compileTemplates() {
	function getTemplate(name) {
		return fs.readFileSync('./site/templates/' + name + '.hbs', { encoding: 'utf8' });
	}

	var names = ['api/content_index', 'api/content_methods', 'api/content_properties',
		'api/content_tabs', 'api/shell', 'api/sidebar', 'api/base'];

	names.forEach(function(name) {
		var template = Handlebars.compile(getTemplate(name));

		templates[name] = function(data) {
			return template(data).trim();
		};
	});
}

function buildNamespacePages(data) {
	var namespaces = data.namespaces;
	var classNames = data.classes.map(function(c) {
		return c.name;
	});
	var namespaceNames = data.namespaces.map(function(n) {
		return n.name;
	});

	namespaces.forEach(function(namespace) {
		var index = templates['api/content_index']({ methods: namespace.methods, properties: namespace.properties });
		var methodList = templates['api/content_methods']({ methods: namespace.methods });
		var propertyList = templates['api/content_properties']({ properties: namespace.properties });

		var tabs = templates['api/content_tabs']({ index: index, properties: propertyList, methods: methodList });
		var sidebar = buildSidebar(classNames, namespaceNames, null, namespace.name);

		var page = templates['api/shell']({
			sidebar: sidebar,
			content: tabs,
			namespace: {
				name: namespace.name,
				description: namespace.description
			}
		});
		var file = templates['api/base']({ body: page });

		fs.writeFileSync('site_build/api/' + namespace.name + '.html', file);
	});
}

function buildClassPages(data) {
	var classes = data.classes;
	var classNames = data.classes.map(function(c) {
		return c.name;
	});
	var namespaceNames = data.namespaces.map(function(n) {
		return n.name;
	});

	classes.forEach(function(c) {
		var index = templates['api/content_index']({ methods: c.methods, properties: c.properties });
		var methodList = templates['api/content_methods']({ methods: c.methods });
		var propertyList = templates['api/content_properties']({ properties: c.properties });

		var tabs = templates['api/content_tabs']({ index: index, properties: propertyList, methods: methodList });
		var sidebar = buildSidebar(classNames, namespaceNames, c.name, null);

		var page = templates['api/shell']({ sidebar: sidebar, content: tabs, 'class': c });
		var file = templates['api/base']({ body: page });

		fs.writeFileSync('site_build/api/' + c.name + '.html', file);
	});
}

function buildSidebar(classNames, namespaceNames, currentClass, currentNamespace) {
	var classes = classNames.map(function(className) {
		return {
			name: className,
			active: className === currentClass
		};
	});

	var namespaces = namespaceNames.map(function(namespace) {
		return {
			name: namespace,
			active: currentNamespace === namespace
		};
	});

	return templates['api/sidebar']({
		classes: classes,
		namespaces: namespaces
	});
}

<|start_filename|>src/model/relationship.js<|end_filename|>
import Ember from 'ember';
import Relationship from 'ember-graph/relationship/relationship';
import RelationshipStore from 'ember-graph/relationship/relationship_store';
import EmberGraphSet from 'ember-graph/util/set';
import copy from 'ember-graph/util/copy';

import { computed } from 'ember-graph/util/computed';

import {
	groupRecords,
	arrayContentsEqual
} from 'ember-graph/util/util';

import {
	RelationshipTypes
} from 'ember-graph/constants';

var HAS_ONE_KEY = RelationshipTypes.HAS_ONE_KEY;
var HAS_MANY_KEY = RelationshipTypes.HAS_MANY_KEY;

var CLIENT_STATE = Relationship.CLIENT_STATE;
var SERVER_STATE = Relationship.SERVER_STATE;
var DELETED_STATE = Relationship.DELETED_STATE;

var HAS_ONE_GETTER = function(key) {
	return this.getHasOneValue(key.substring(1), false);
};

var HAS_MANY_GETTER = function(key) {
	return this.getHasManyValue(key.substring(1), false);
};

var createRelationship = function(name, kind, options) {
	Ember.assert('Invalid relatedType', Ember.typeOf(options.relatedType) === 'string');
	Ember.assert('Invalid inverse', options.inverse === null || Ember.typeOf(options.inverse) === 'string');

	var meta = {
		// the 'real' relationship (without _) is the relationship
		isRelationship: false,
		kind: kind,
		isRequired: (options.hasOwnProperty('defaultValue') ? false : options.isRequired !== false),
		defaultValue: options.defaultValue,
		relatedType: options.relatedType,
		inverse: options.inverse,
		isReadOnly: options.readOnly === true || options.serverOnly === true,
		isPolymorphic: options.polymorphic === true,
		isServerOnly: options.serverOnly === true,

		getDefaultValue: function() {
			var defaultValue = this.defaultValue || (kind === HAS_MANY_KEY ? [] : null);
			return (typeof defaultValue === 'function' ? defaultValue() : defaultValue);
		}
	};

	Ember.assert('defaultValue for hasMany must be an array.',
			meta.kind === HAS_ONE_KEY || Ember.isArray(meta.getDefaultValue()));
	Ember.assert('defaultValue for hasOne must be null or a string.',
			meta.kind === HAS_MANY_KEY || meta.getDefaultValue() === null ||
			Ember.typeOf(meta.getDefaultValue()) === 'string');

	return {
    relationship: computed(`relationships.{client,deleted,server}.${name}`, { 'get': meta.kind === HAS_MANY_KEY ? HAS_MANY_GETTER : HAS_ONE_GETTER }).meta(meta),
    meta
  };
};

var RelationshipClassMethods = {

	/**
	 * Fetch the metadata for a relationship property.
	 *
	 * @method metaForRelationship
	 * @for Model
	 * @param {String} relationshipName
	 * @return {Object}
	 * @static
	 */
	metaForRelationship() {
		return this.metaForProperty(...arguments);
	},

	/**
	 * Determines the kind (multiplicity) of the given relationship.
	 *
	 * @method relationshipKind
	 * @for Model
	 * @param {String} name
	 * @returns {String} `hasMany` or `hasOne`
	 * @static
	 */
	relationshipKind: function(name) {
		return this.metaForRelationship(name).kind;
	},

	/**
	 * Calls the callback for each relationship defined on the model.
	 *
	 * @method eachRelationship
	 * @for Model
	 * @param {Function} callback Function that takes `name` and `meta` parameters
	 * @param [binding] Object to use as `this`
	 * @static
	 */
	eachRelationship: function(callback, binding) {
		var classProto = this.prototype;

		while (classProto && classProto.metaMap) {
			if (classProto.metaMap['_all']) {
				for (var j = 0, len = classProto.metaMap['_all'].length; j < len; ++j) {
					var name = classProto.metaMap['_all'][j];
					callback.call(binding, name, classProto.metaMap[name]);
				}
			}

			if (typeof Object.getPrototypeOf !== 'function') {
				if (typeof 'test'.__proto__ === 'object') { // eslint-disable-line no-proto
					Object.getPrototypeOf = function(object) {
						return object.__proto__;  // eslint-disable-line no-proto
					};
				} else {
					Object.getPrototypeOf = function(object) {
						// May break if the constructor has been tampered with
						return object.constructor.prototype;
					};
				}
			}
			classProto = Object.getPrototypeOf(classProto);
		}
	},

	declareRelationships: function(relationships) {
		var obj = {};

		Ember.runInDebug(function() {
			var disallowedNames = EmberGraphSet.create();
			disallowedNames.addObjects(['id', 'type', 'content', 'length', 'model', 'metaMap']);

			Object.keys(relationships).forEach(function(name) {
				Ember.assert('`' + name + '` cannot be used as a relationship name.', !disallowedNames.contains(name));
				Ember.assert('A relationship name cannot start with an underscore.', name.charAt(0) !== '_');
				Ember.assert('Relationship names must start with a lowercase letter.', name.charAt(0).match(/[a-z]/));
			});
		});

		Object.keys(relationships).forEach(function(name) {
			const { relationship: rel, meta: relMeta } = createRelationship(name, relationships[name].kind, relationships[name].options);
			obj['_' + name] = rel;
			var meta = copy(relMeta, true);
			var relatedType = meta.relatedType;

			var relationship;

			// We're not going to close over many variables for the sake of speed
			if (meta.kind === HAS_ONE_KEY) {
				relationship = function(key) {
					var value = this.get('_' + key);
					return (value ? this.get('store').find(value.type, value.id) : null);
				};
			} else if (!meta.isPolymorphic) {
				relationship = function(key) {
					var value = this.get('_' + key);
					var ids = value.map((item) => item.id);
					return this.get('store').find(relatedType, ids);
				};
			} else {
				relationship = function(key) {
					var store = this.get('store');
					var value = this.get('_' + key);
					var groups = groupRecords(value);
					var promises = groups.map(function(group) {
						var ids = group.map(function(item) {
							return item.id;
						});
						return store.find(group[0].type, ids);
					});
					return Ember.RSVP.Promise.all(promises).then(function(groups) {
						return groups.reduce(function(array, group) {
							return array.concat(group);
						}, []);
					});
				};
			}

			meta.isRelationship = true;
			obj[name] = computed(`_${name}`, { 'get': relationship }).meta(meta);
			if (!obj['metaMap']) {
				obj['metaMap'] = [];
				obj['metaMap']['_all'] = [];
			}
			obj['metaMap'][name] = meta;
			obj['metaMap']['_all'].push(name);
		});

		this.reopen(obj);
	}

};

var RelationshipPublicMethods = {

	areRelationshipsDirty: computed('relationships.{client,deleted}.length', {
		get() {
			return this.get('relationships.client.length') > 0 || this.get('relationships.deleted.length') > 0;
		}
	}),

	/**
	 * Gets all of the dirty relationships for the model. The object returned
	 * is of the format:
	 *
	 * ```js
	 * {
	 *     relationshipName: [oldValue, newValue]
	 * }
	 * ```
	 *
	 * @method changedRelationships
	 * @for Model
	 * @returns {Object}
	 */
	changedRelationships: function() {
		var changes = {};

		this.constructor.eachRelationship(function(name, meta) {
			if (meta.isReadOnly) {
				return;
			}

			if (meta.kind === HAS_MANY_KEY) {
				const oldVal = this.getHasManyValue(name, true);
				const newVal = this.getHasManyValue(name, false);

				if (!arrayContentsEqual(oldVal, newVal)) {
					changes[name] = [oldVal, newVal];
				}
			} else {
				const oldVal = this.getHasOneValue(name, true);
				const newVal = this.getHasOneValue(name, false);

				if (!oldVal && !newVal) {
					return;
				}

				if ((!oldVal && newVal) || (oldVal && !newVal) ||
					(oldVal.typeKey !== newVal.typeKey || oldVal.id !== newVal.id)) {
					changes[name] = [oldVal, newVal];
				}
			}
		}, this);

		return changes;
	},

	/**
	 * Resets all of the relationships to the last known server state.
	 *
	 * @method rollbackRelationships
	 * @for Model
	 */
	rollbackRelationships: function() {
		Ember.changeProperties(() => {
			var store = this.get('store');

			var client = this.get('relationships').getRelationshipsByState(CLIENT_STATE);
			client.forEach(function(relationship) {
				store.deleteRelationship(relationship);
			});

			var deleted = this.get('relationships').getRelationshipsByState(DELETED_STATE);
			deleted.forEach(function(relationship) {
				store.changeRelationshipState(relationship, SERVER_STATE);
			});
		});
	},

	/**
	 * Adds a value to the given hasMany relationship. The `id` parameter can
	 * be either a string or a model instance. If it's a model, the ID and
	 * type and detected automatically. If it's a string, the type should
	 * be given explicitly for polymorphic relationships. If the relationship
	 * is non-polymorphic, the declared `relatedType` will be used.
	 *
	 * @method addToRelationship
	 * @for Model
	 * @param {String} relationshipName
	 * @param {String|Model} id
	 * @param {String} [polymorphicType] Defaults to declared `relatedType`
	 */
	addToRelationship: function(relationshipName, id, polymorphicType) {
		var meta = this.constructor.metaForRelationship(relationshipName);
		if (meta.kind !== HAS_MANY_KEY) {
			throw new Ember.Error('`addToRelationship` called on hasOne relationship.');
		}

		if (meta.isReadOnly && !this.get('isNew')) {
			throw new Ember.Error('Can\'t modify a read-only relationship.');
		}

		Ember.changeProperties(() => {
			this.set('initializedRelationships.' + relationshipName, true);

			const store = this.get('store');

			// If the type wasn't provided, fill it in based on the inverse
			if (Ember.typeOf(id) !== 'string') {
				polymorphicType = Ember.get(id, 'typeKey'); // eslint-disable-line no-param-reassign
				id = Ember.get(id, 'id'); // eslint-disable-line no-param-reassign
			} else if (Ember.typeOf(polymorphicType) !== 'string') {
				polymorphicType = meta.relatedType; // eslint-disable-line no-param-reassign
			}

			var otherModel = store.modelFor(polymorphicType);
			var otherMeta = (meta.inverse === null ? null : otherModel.metaForRelationship(meta.inverse));
			var currentValues = this.getHasManyRelationships(relationshipName, false);
			var serverValues = this.getHasManyRelationships(relationshipName, true);

			// Check to see if the records are already connected
			for (let i = 0; i < currentValues.length; ++i) {
				if (currentValues[i].otherType(this) === polymorphicType && currentValues[i].otherId(this) === id) {
					return;
				}
			}

			// If the inverse is null or a hasMany, we can create the relationship without conflict
			if (meta.inverse === null || otherMeta.kind === HAS_MANY_KEY) {
				// Check for delete relationships first
				for (let i = 0; i < serverValues.length; ++i) {
					if (serverValues[i].otherType(this) === polymorphicType && serverValues[i].otherId(this) === id) {
						store.changeRelationshipState(serverValues[i], SERVER_STATE);
						return;
					}
				}

				store.createRelationship(this.typeKey, this.get('id'), relationshipName,
					polymorphicType, id, meta.inverse, CLIENT_STATE);

				return;
			}

			// Make sure there are no conflicts on the other side since it's a hasOne
			var otherValues = store.sortHasOneRelationships(polymorphicType, id, meta.inverse);
			if (otherValues[SERVER_STATE]) {
				store.changeRelationshipState(otherValues[SERVER_STATE], DELETED_STATE);
			} else if (otherValues[CLIENT_STATE]) {
				store.deleteRelationship(otherValues[CLIENT_STATE]);
			}

			// Check for any deleted relationships that match the one we need
			for (let i = 0; i < serverValues.length; ++i) {
				if (serverValues[i].otherType(this) === polymorphicType && serverValues[i].otherId(this) === id) {
					store.changeRelationshipState(serverValues[i], SERVER_STATE);
					return;
				}
			}

			// If all else fails, create a relationship
			store.createRelationship(this.typeKey, this.get('id'), relationshipName,
				polymorphicType, id, meta.inverse, CLIENT_STATE);
		});
	},

	/**
	 * Removes a value from the given hasMany relationship. The `id` parameter can
	 * be either a string or a model instance. If it's a model, the ID and
	 * type and detected automatically. If it's a string, the type should
	 * be given explicitly for polymorphic relationships. If the relationship
	 * is non-polymorphic, the declared `relatedType` will be used.
	 *
	 * @method removeFromRelationship
	 * @for Model
	 * @param {String} relationshipName
	 * @param {String|Model} id
	 * @param {String} [polymorphicType] Defaults to declared `relatedType`
	 */
	removeFromRelationship: function(relationshipName, id, polymorphicType) {
		var meta = this.constructor.metaForRelationship(relationshipName);
		if (meta.kind !== HAS_MANY_KEY) {
			throw new Ember.Error('`removeFromRelationship` called on hasOne relationship.');
		}

		if (meta.isReadOnly && !this.get('isNew')) {
			throw new Ember.Error('Can\'t modify a read-only relationship.');
		}

		Ember.changeProperties(() => {
			// If the type wasn't provided, fill it in based on the inverse
			if (Ember.typeOf(id) !== 'string') {
				polymorphicType = Ember.get(id, 'typeKey'); // eslint-disable-line no-param-reassign
				id = Ember.get(id, 'id'); // eslint-disable-line no-param-reassign
			} else if (Ember.typeOf(polymorphicType) !== 'string') {
				polymorphicType = meta.relatedType; // eslint-disable-line no-param-reassign
			}

			var relationships = this.getHasManyRelationships(relationshipName, false);
			for (var i = 0; i < relationships.length; ++i) {
				if (relationships[i].otherType(this) === polymorphicType && relationships[i].otherId(this) === id) {
					if (relationships[i].get('state') === CLIENT_STATE) {
						this.get('store').deleteRelationship(relationships[i]);
					} else {
						this.get('store').changeRelationshipState(relationships[i], DELETED_STATE);
					}

					break;
				}
			}
		});
	},

	/**
	 * Sets the value of the given hasOne relationship. The `id` parameter can
	 * be either a string or a model instance. If it's a model, the ID and
	 * type and detected automatically. If it's a string, the type should
	 * be given explicitly for polymorphic relationships. If the relationship
	 * is non-polymorphic, the declared `relatedType` will be used.
	 *
	 * @method setHasOneRelationship
	 * @for Model
	 * @param {String} relationshipName
	 * @param {String|Model} id
	 * @param {String} [polymorphicType] Defaults to declared `relatedType`
	 */
	setHasOneRelationship: function(relationshipName, id, polymorphicType) {
		var meta = this.constructor.metaForRelationship(relationshipName);
		if (meta.kind !== HAS_ONE_KEY) {
			throw new Ember.Error('`setHasOneRelationship` called on hasMany relationship.');
		}

		if (meta.isReadOnly && !this.get('isNew')) {
			throw new Ember.Error('Can\'t modify a read-only relationship.');
		}

		Ember.changeProperties(() => {
			this.set('initializedRelationships.' + relationshipName, true);

			var store = this.get('store');

			// If the type wasn't provided, fill it in based on the inverse
			if (Ember.typeOf(id) !== 'string') {
				polymorphicType = id.typeKey; // eslint-disable-line no-param-reassign
				id = id.get('id'); // eslint-disable-line no-param-reassign
			} else if (Ember.typeOf(polymorphicType) !== 'string') {
				polymorphicType = meta.relatedType; // eslint-disable-line no-param-reassign
			}

			var otherModel = store.modelFor(polymorphicType);
			var otherMeta = (meta.inverse === null ? null : otherModel.metaForRelationship(meta.inverse));
			var currentRelationships = store.sortHasOneRelationships(this.typeKey, this.get('id'), relationshipName);

			// First make sure they're not already connected
			if (currentRelationships[SERVER_STATE] &&
				currentRelationships[SERVER_STATE].otherType(this) === polymorphicType &&
				currentRelationships[SERVER_STATE].otherId(this) === id) {
				return;
			}

			if (currentRelationships[CLIENT_STATE] &&
				currentRelationships[CLIENT_STATE].otherType(this) === polymorphicType &&
				currentRelationships[CLIENT_STATE].otherId(this) === id) {
				return;
			}

			// They're not connected, so we definitely have to get rid of the current value
			if (currentRelationships[SERVER_STATE]) {
				store.changeRelationshipState(currentRelationships[SERVER_STATE], DELETED_STATE);
			} else if (currentRelationships[CLIENT_STATE]) {
				store.deleteRelationship(currentRelationships[CLIENT_STATE]);
			}

			// If the inverse is null or a hasMany, we can just create the relationship
			if (meta.inverse === null || otherMeta.kind === HAS_MANY_KEY) {
				var temp1;
				// Check for a deleted relationship first
				for (var i = 0; i < currentRelationships[DELETED_STATE].length; ++i) {
					temp1 = currentRelationships[DELETED_STATE][i];
					if (temp1.otherType(this) === polymorphicType && temp1.otherId(this) === id) {
						store.changeRelationshipState(temp1, SERVER_STATE);
						return;
					}
				}

				// If we can't find one, just create a new relationship
				store.createRelationship(this.typeKey, this.get('id'), relationshipName,
					polymorphicType, id, meta.inverse, CLIENT_STATE);

				return;
			}

			// We have to make sure there are no conflicts on the other side, since it's also a hasOne
			var otherRelationships = store.sortHasOneRelationships(polymorphicType, id, meta.inverse);
			if (otherRelationships[SERVER_STATE]) {
				store.changeRelationshipState(otherRelationships[SERVER_STATE], DELETED_STATE);
			} else if (otherRelationships[CLIENT_STATE]) {
				store.deleteRelationship(otherRelationships[CLIENT_STATE]);
			}

			// Finally, check for a deleted relationship between the two
			var temp2;
			for (var j = 0; j < currentRelationships[DELETED_STATE].length; ++j) {
				temp2 = currentRelationships[DELETED_STATE][j];
				if (temp2.otherType(this) === polymorphicType && temp2.otherId(this) === id) {
					store.changeRelationshipState(temp2, SERVER_STATE);
					return;
				}
			}

			// If all else fails, create a relationship
			store.createRelationship(this.typeKey, this.get('id'), relationshipName,
				polymorphicType, id, meta.inverse, CLIENT_STATE);
		});
	},

	/**
	 * Clears the value of the given hasOne relationship.
	 *
	 * @method clearHasOneRelationship
	 * @for Model
	 * @param {String} relationshipName
	 */
	clearHasOneRelationship: function(relationshipName) {
		var meta = this.constructor.metaForRelationship(relationshipName);
		if (meta.kind !== HAS_ONE_KEY) {
			throw new Ember.Error('`clearHasOneRelationship` called on hasMany relationship.');
		}

		if (meta.isReadOnly && !this.get('isNew')) {
			throw new Ember.Error('Can\'t modify a read-only relationship.');
		}

		Ember.changeProperties(() => {
			var relationship = this.getHasOneRelationship(relationshipName, false);
			if (relationship) {
				if (relationship.get('state') === CLIENT_STATE) {
					this.get('store').deleteRelationship(relationship);
				} else {
					this.get('store').changeRelationshipState(relationship, DELETED_STATE);
				}
			}
		});
	},

	/**
	 * Determines whether or not every required attribute has been initialized.
	 * If this returns false, the record cannot be persisted to the server.
	 *
	 * @method areRelationshipsInitialized
	 * @for Model
	 * @return {Boolean}
	 */
	areRelationshipsInitialized: function() {
		var initialized = true;

		this.constructor.eachRelationship(function(name, meta) {
			if (meta.isRequired && !meta.isServerOnly) {
				initialized = initialized && this.isRelationshipInitialized(name);
			}
		}, this);

		return initialized;
	},

	/**
	 * Determines if the given relationship has been initialized yet.
	 * Always returns `true` for non-new records.
	 *
	 * @method isRelationshipInitialized
	 * @for Model
	 * @param {String} relationshipName
	 * @return {Boolean}
	 */
	isRelationshipInitialized: function(relationshipName) {
		return !this.get('isNew') || !!this.get('initializedRelationships.' + relationshipName);
	}

};

var RelationshipPrivateMethods = {

	/**
	 * Stores all of the relationships currently connected to this record.
	 * The model itself should only read from this object. All additions
	 * and deletions are handled by the store (so they can be reciprocated).
	 *
	 * @type RelationshipMap
	 * @private
	 */
	relationships: null,

	/**
	 * Stores which relationships have had values set.
	 *
	 * @type Object
	 * @private
	 */
	initializedRelationships: null,

	initializeRelationshipStoreAndStatus: Ember.on('init', function() {
		this.setProperties({
			relationships: RelationshipStore.create(),
			initializedRelationships: {}
		});
	}),

	getHasOneRelationship: function(name, server) {
		var relationships;

		if (server) {
			relationships = this.get('relationships').getServerRelationships(name);
		} else {
			relationships = this.get('relationships').getCurrentRelationships(name);
		}

		if (relationships.length <= 0) {
			return null;
		} else {
			return relationships[0];
		}
	},

	getHasOneValue: function(name, server) {
		var relationship = this.getHasOneRelationship(name, server);

		if (relationship === null) {
			return null;
		} else {
			return {
				type: relationship.otherType(this),
				id: relationship.otherId(this)
			};
		}
	},

	getHasManyRelationships: function(name, server) {
		if (server) {
			return this.get('relationships').getServerRelationships(name);
		} else {
			return this.get('relationships').getCurrentRelationships(name);
		}
	},

	getHasManyValue: function(name, server) {
		return this.getHasManyRelationships(name, server).map(function(relationship) {
			return {
				type: relationship.otherType(this),
				id: relationship.otherId(this)
			};
		}, this);
	}

};

export {
	RelationshipClassMethods,
	RelationshipPublicMethods,
	RelationshipPrivateMethods,
	HAS_ONE_KEY,
	HAS_MANY_KEY
};


<|start_filename|>tasks/upload_builds_to_s3.js<|end_filename|>
'use strict';

var fs = require('fs');
var AWS = require('aws-sdk');
var AdmZip = require('adm-zip');
var execSync = require('child_process').execSync;

module.exports = function(grunt) {
	grunt.registerTask('upload_builds_to_s3', function() {
		var done = this.async();
		var hash = execSync('git rev-parse HEAD').toString().trim();
		var bowerConfig = fs.readFileSync('./bower.json', { encoding: 'utf8' });
		var debugBuild = fs.readFileSync('./dist/ember-graph.js', { encoding: 'utf8' });
		var productionBuild = fs.readFileSync('./dist/ember-graph.prod.js', { encoding: 'utf8' });
		var minifiedBuild = fs.readFileSync('./dist/ember-graph.min.js', { encoding: 'utf8' });

		var zip = new AdmZip();
		zip.addFile('bower.json', new Buffer(bowerConfig));
		zip.addFile('ember-graph.js', new Buffer(debugBuild));
		zip.addFile('ember-graph.prod.js', new Buffer(productionBuild));
		zip.addFile('ember-graph.min.js', new Buffer(minifiedBuild));
		var archive = zip.toBuffer();

		var count = 0;
		var counter = function(success, fileName) {
			count = count + 1;

			if (!success) {
				console.log('Error uploading file to S3: ' + fileName);
			}

			if (count >= 8) {
				done();
			}
		};

		var s3 = new AWS.S3();
		uploadFile(s3, 'latest/ember-graph.js', debugBuild, counter);
		uploadFile(s3, 'latest/ember-graph.prod.js', productionBuild, counter);
		uploadFile(s3, 'latest/ember-graph.min.js', minifiedBuild, counter);
		uploadFile(s3, 'latest/ember-graph.zip', archive, counter);

		uploadFile(s3, hash + '/ember-graph.min.js', minifiedBuild, counter);
		uploadFile(s3, hash + '/ember-graph.js', debugBuild, counter);
		uploadFile(s3, hash + '/ember-graph.prod.js', productionBuild, counter);
		uploadFile(s3, hash + '/ember-graph.zip', archive, counter);
	});
};

function uploadFile(s3, fileName, contents, callback) {
	s3.putObject({
		ACL: 'public-read',
		Body: contents,
		Bucket: 'ember-graph-builds',
		ContentType: (fileName.endsWith('.zip') ? 'application/zip' : 'application/javascript'),
		Key: fileName
	}, function(err, data) {
		callback(!err, fileName);
	});
}

<|start_filename|>test/model/attribute.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Model Attribute Test', {
		setup: function() {
			store = setupStore({
				test: EG.Model.extend({
					name: EG.attr({
						type: 'string',
						readOnly: true
					}),

					posts: EG.attr({
						type: 'number',
						defaultValue: 0
					}),

					birthday: EG.attr('date')
				})
			});
		}
	});

	test('The class properly detects every attribute (and only those attributes)', function() {
		expect(1);

		var TestModel = store.modelFor('test');
		var expectedAttributes = EG.Set.create();
		expectedAttributes.addObjects(['name', 'posts', 'birthday']);

		ok(Em.get(TestModel, 'attributes').isEqual(expectedAttributes));
	});

	test('The class knows which properties are attributes', function() {
		expect(3);

		var TestModel = store.modelFor('test');

		ok(TestModel.isAttribute('name'));
		ok(!TestModel.isAttribute('foofoo'));
		ok(!TestModel.isAttribute('POSTS'));
	});

	test('Creating an object loads the attributes correctly', function() {
		expect(3);

		var birthday = new Date('1970-01-01');
		var model = store.createRecord('test', {
			name: 'Bob',
			posts: 16,
			birthday: birthday
		});

		ok(model.get('name') === 'Bob');
		ok(model.get('posts') === 16);
		ok(model.get('birthday').getTime() === birthday.getTime());
	});

	test('Creating an object leaves attributes uninitialized', function() {
		expect(3);

		var model = store.createRecord('test');
		ok(!model.isAttributeInitialized('name'));
		ok(!model.isAttributeInitialized('posts'));
		ok(!model.isAttributeInitialized('birthday'));
	});

	if (window.DEBUG_MODE === true) {
		test('Creating an object with a missing required property throws', function() {
			expect(1);

			throws(function() {
				store.createRecord('test', {
					name: 'Bob'
				});
			});
		});

		test('Creating an object with an invalid property value throws', function() {
			expect(1);

			throws(function() {
				store.createRecord('test', {
					name: 'Bob',
					birthday: 52
				});
			});
		});
	}

	test('Setting attributes sets them correctly', function() {
		expect(2);

		var model = store.createRecord('test', {
			name: 'Bob',
			posts: 18,
			birthday: new Date()
		});

		model.set('posts', 42);
		ok(model.get('posts') === 42);

		model.set('birthday', null);
		ok(model.get('birthday') === null);
	});

	test('Setting a read-only property on a saved record throws', function() {
		expect(1);

		store.pushPayload({
			test: [{
				id: '50',
				name: 'Bob',
				birthday: new Date(),
				posts: 10
			}]
		});

		var model = store.getRecord('test', '50');

		throws(function() {
			model.set('name', '');
		});
	});

	test('Setting a read-only property on a new record doesn\'t throw', function() {
		expect(1);

		var model = store.createRecord('test', {
			name: 'Bob',
			birthday: new Date()
		});

		model.set('name', 'Robert');
		strictEqual(model.get('name'), 'Robert');
	});

	test('Setting value to undefined fails', function() {
		expect(1);

		var model = store.createRecord('test', {
			name: 'Bob',
			birthday: null,
			posts: 0
		});

		model.set('posts', undefined);
		ok(model.get('posts') === 0);
	});

	test('Getting changed attributes returns the correct values', function() {
		expect(4);

		var birthday = new Date();
		store.pushPayload({
			test: [{
				id: '500',
				name: 'Bob',
				birthday: birthday
			}]
		});
		var model = store.getRecord('test', '500');

		model.set('posts', 25);
		model.set('birthday', null);

		var changed = model.changedAttributes();

		strictEqual(changed.posts[0], 0);
		strictEqual(changed.posts[1], 25);
		strictEqual(changed.birthday[0].getTime(), birthday.getTime());
		strictEqual(changed.birthday[1], null);
	});

	test('Getting changed attributes doesn\'t include extra values', function() {
		expect(3);

		var birthday = new Date();
		store.pushPayload({
			test: [{
				id: '500',
				name: 'Bob',
				birthday: birthday
			}]
		});
		var model = store.getRecord('test', '500');

		model.set('posts', 25);

		var changed = model.changedAttributes();

		deepEqual(Object.keys(changed), ['posts']);
		strictEqual(changed.posts[0], 0);
		strictEqual(changed.posts[1], 25);
	});

	test('Rolling back attributes works correctly', function() {
		expect(3);

		store.pushPayload({
			test: [{
				id: '500',
				name: 'Bob',
				birthday: null
			}]
		});
		var model = store.getRecord('test', '500');

		model.set('posts', 25);
		model.set('birthday', new Date());
		model.rollbackAttributes();

		var changed = model.changedAttributes();

		deepEqual(Object.keys(changed), []);
		strictEqual(model.get('posts'), 0);
		strictEqual(model.get('birthday'), null);
	});

	test('metaForAttribute returns the correct metadata', function() {
		expect(2);

		var TestModel = store.modelFor('test');
		var meta = TestModel.metaForRelationship('name');

		strictEqual(meta.isAttribute, true);
		strictEqual(meta.isRequired, true);
	});

	test('Setting attributes dirties the record (old record)', function() {
		expect(2);

		store.pushPayload({
			test: [{
				id: '500',
				name: 'Bob',
				birthday: null
			}]
		});
		var model = store.getRecord('test', '500');

		strictEqual(model.get('isDirty'), false);

		model.set('posts', 42);
		model.set('birthday', null);

		strictEqual(model.get('isDirty'), true);
	});

	test('Rolling back attributes cleans the record', function() {
		expect(3);

		store.pushPayload({
			test: [{
				id: '500',
				name: 'Bob',
				birthday: new Date()
			}]
		});
		var model = store.getRecord('test', '500');

		strictEqual(model.get('isDirty'), false);

		model.set('posts', 42);
		model.set('birthday', null);

		strictEqual(model.get('isDirty'), true);

		model.rollbackAttributes();

		strictEqual(model.get('isDirty'), false);
	});
})();



<|start_filename|>tasks/setup_site_structure.js<|end_filename|>
'use strict';

var execSync = require('child_process').execSync;

module.exports = function(grunt) {
	grunt.registerTask('setup_site_structure', function() {
		execSync('rm -rf site_build');
		execSync('mkdir -p site_build/api');

		execSync('cp -r site/fonts site_build/');
		execSync('cp -r site/javascripts site_build/');
		execSync('cp -r site/index.html site_build/');

		execSync('mkdir -p site_build/stylesheets');
	});
};


<|start_filename|>src/util/inflector.js<|end_filename|>
import Ember from 'ember';

/*
 I took the rules in this code from inflection.js, whose license can be found below.
 */

/*
 Copyright (c) 2010 <NAME> (<EMAIL>)

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 */

const uncountableWords = [
	'equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep', 'moose', 'deer', 'news'
];

const pluralRules = [
	[/(m)an$/gi,                 '$1en'],
	[/(pe)rson$/gi,              '$1ople'],
	[/(child)$/gi,               '$1ren'],
	[/^(ox)$/gi,                 '$1en'],
	[/(ax|test)is$/gi,           '$1es'],
	[/(octop|vir)us$/gi,         '$1i'],
	[/(alias|status)$/gi,        '$1es'],
	[/(bu)s$/gi,                 '$1ses'],
	[/(buffal|tomat|potat)o$/gi, '$1oes'],
	[/([ti])um$/gi,              '$1a'],
	[/sis$/gi,                   'ses'],
	[/(?:([^f])fe|([lr])f)$/gi,  '$1$2ves'],
	[/(hive)$/gi,                '$1s'],
	[/([^aeiouy]|qu)y$/gi,       '$1ies'],
	[/(x|ch|ss|sh)$/gi,          '$1es'],
	[/(matr|vert|ind)ix|ex$/gi,  '$1ices'],
	[/([m|l])ouse$/gi,           '$1ice'],
	[/(quiz)$/gi,                '$1zes'],
	[/s$/gi,                     's'],
	[/$/gi,                      's']
];

const singularRules = [
	[/(m)en$/gi,                                                        '$1an'],
	[/(pe)ople$/gi,                                                     '$1rson'],
	[/(child)ren$/gi,                                                   '$1'],
	[/([ti])a$/gi,                                                      '$1um'],
	[/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/gi, '$1$2sis'],
	[/(hive)s$/gi,                                                      '$1'],
	[/(tive)s$/gi,                                                      '$1'],
	[/(curve)s$/gi,                                                     '$1'],
	[/([lr])ves$/gi,                                                    '$1f'],
	[/([^fo])ves$/gi,                                                   '$1fe'],
	[/([^aeiouy]|qu)ies$/gi,                                            '$1y'],
	[/(s)eries$/gi,                                                     '$1eries'],
	[/(m)ovies$/gi,                                                     '$1ovie'],
	[/(x|ch|ss|sh)es$/gi,                                               '$1'],
	[/([m|l])ice$/gi,                                                   '$1ouse'],
	[/(bus)es$/gi,                                                      '$1'],
	[/(o)es$/gi,                                                        '$1'],
	[/(shoe)s$/gi,                                                      '$1'],
	[/(cris|ax|test)es$/gi,                                             '$1is'],
	[/(octop|vir)i$/gi,                                                 '$1us'],
	[/(alias|status)es$/gi,                                             '$1'],
	[/^(ox)en/gi,                                                       '$1'],
	[/(vert|ind)ices$/gi,                                               '$1ex'],
	[/(matr)ices$/gi,                                                   '$1ix'],
	[/(quiz)zes$/gi,                                                    '$1'],
	[/s$/gi,                                                            '']
];

const apply = function(str, rules) {
	if (uncountableWords.indexOf(str) >= 0) {
		return str;
	}

	for (let i = 0; i < rules.length; i = i + 1) {
		if (str.match(rules[i][0])) {
			return str.replace(rules[i][0], rules[i][1]);
		}
	}

	return str;
};

const PLURALIZE_CACHE = {};
function pluralize(str) {
	if (!PLURALIZE_CACHE[str]) {
		PLURALIZE_CACHE[str] = apply(str, pluralRules);
	}

	return PLURALIZE_CACHE[str];
}

const SINGULARIZE_CACHE = {};
function singularize(str) {
	if (!SINGULARIZE_CACHE[str]) {
		SINGULARIZE_CACHE[str] = apply(str, singularRules);
	}

	return SINGULARIZE_CACHE[str];
}

function overridePluralRule(singular, plural) {
	PLURALIZE_CACHE[singular] = plural;
}

function overrideSingularRule(plural, singular) {
	SINGULARIZE_CACHE[plural] = singular;
}

if (Ember.ENV.EXTEND_PROTOTYPES === true || Ember.ENV.EXTEND_PROTOTYPES.String) {
	String.prototype.pluralize = String.prototype.pluralize || function() {
		return pluralize(this);
	};

	String.prototype.singularize = String.prototype.singularize || function() {
		return singularize(this);
	};
}

export {
	pluralize,
	singularize,
	overridePluralRule,
	overrideSingularRule
};


<|start_filename|>src/store/lookup.js<|end_filename|>
import Ember from 'ember';

import { deprecateMethod } from 'ember-graph/util/util';

export default {

	/**
	 * Stores the models used so far. This not ony caches them so we don't
	 * have to hit the container, but it also let's use know that the
	 * typeKey has been property injected into them.
	 *
	 * @property modelCache
	 * @type {Object}
	 * @final
	 * @private
	 */
	modelCache: {},

	/**
	 * Stores attribute types as they're looked up in the container.
	 * @property attributeTypeCache
	 * @type {Object}
	 * @final
	 * @private
	 */
	attributeTypeCache: {},

	/**
	 * Stores adapters as they're looked up in the container.
	 *
	 * @property adapterCache
	 * @type Object
	 * @final
	 * @private
	 */
	adapterCache: {},

	/**
	 * Stores serializers as they're looked up in the container.
	 *
	 * @property adapterCache
	 * @type Object
	 * @final
	 * @private
	 */
	serializerCache: {},

	initializeLookupCaches: Ember.on('init', function() {
		this.setProperties({
			modelCache: {},
			attributeTypeCache: {},
			adapterCache: {},
			serializerCache: {}
		});
	}),

	modelForType: deprecateMethod('`modelForType` deprecated in favor of `modelFor`', 'modelFor'),

	/**
	 * Looks up the model for the specified typeKey. The `typeKey` property
	 * isn't available on the class or its instances until the type is
	 * looked up with this method for the first time.
	 *
	 * @method modelFor
	 * @param {String} typeKey
	 * @return {Class}
	 */
	modelFor(typeKey) {
		const modelCache = this.get('modelCache');

		if (!modelCache[typeKey]) {
			const model = Ember.getOwner(this).factoryFor(`model:${typeKey}`);
			if (!model) {
				throw new Ember.Error(`Cannot find model class with typeKey: ${typeKey}`);
			}

			model.class.reopen({ typeKey });
			model.class.reopenClass({ typeKey });
			modelCache[typeKey] = model.class;
		}

		return modelCache[typeKey];
	},

	/**
	 * Returns an `AttributeType` instance for the given named type.
	 *
	 * @method attributeTypeFor
	 * @param {String} typeName
	 * @return {AttributeType}
	 */
	attributeTypeFor(typeName) {
		const attributeTypeCache = this.get('attributeTypeCache');

		if (!attributeTypeCache[typeName]) {
			attributeTypeCache[typeName] = Ember.getOwner(this).lookup(`type:${typeName}`);

			if (!attributeTypeCache[typeName]) {
				throw new Ember.Error(`Cannot find attribute type with name: ${typeName}`);
			}
		}

		return attributeTypeCache[typeName];
	},

	/**
	 * Gets the adapter for the specified type. First, it looks for a type-specific
	 * adapter. If one isn't found, it looks for the application adapter. If that
	 * isn't found, it uses the default {{link-to-class 'RESTAdapter'}}.
	 *
	 * Note that this method will cache the results, so your adapter configuration
	 * must be finalized before the app starts up.
	 *
	 * @method adapterFor
	 * @param {String} typeKey
	 * @return {Adapter}
	 * @protected
	 */
	adapterFor(typeKey) {
		const adapterCache = this.get('adapterCache');

		if (!adapterCache[typeKey]) {
			const container = Ember.getOwner(this);

			adapterCache[typeKey] = container.lookup(`adapter:${typeKey}`) ||
					container.lookup('adapter:application') ||
					container.lookup('adapter:rest');
		}

		return adapterCache[typeKey];
	},

	/**
	 * Gets the serializer for the specified type. First, it looks for a type-specific
	 * serializer. If one isn't found, it looks for the application serializer. If that
	 * isn't found, it uses the default {{link-to-class 'JSONSerializer'}}.
	 *
	 * Note that this method will cache the results, so your serializer configuration
	 * must be finalized before the app starts up.
	 *
	 * @method serializerFor
	 * @param {String} typeKey
	 * @return {Serializer}
	 * @protected
	 */
	serializerFor(typeKey) {
		const serializerCache = this.get('serializerCache');

		if (!serializerCache[typeKey]) {
			const container = Ember.getOwner(this);

			serializerCache[typeKey] =
					container.lookup('serializer:' + (typeKey || 'application')) ||
					container.lookup('serializer:application') ||
					container.lookup('serializer:json');
		}

		return serializerCache[typeKey];
	}

};


<|start_filename|>src/util/computed.js<|end_filename|>
import Ember from 'ember';

import { verifyAtLeastEmberVersion } from 'ember-graph/util/compatibility';

const isNewVersion = verifyAtLeastEmberVersion(1, 12, 0);

const oldComputed = (...args) => {
	const dependentProperties = args.slice(0, -1);
	const definition = args[args.length - 1];
	const readOnly = !definition.set;

	if (readOnly) {
		return Ember.computed(...dependentProperties, function(key) {
			return definition.get.call(this, key);
		}).readOnly();
	} else {
		return Ember.computed(...dependentProperties, function(key, value) {
			if (arguments.length > 1) {
				definition.set.call(this, key, value);
			}

			return definition.get.call(this, key);
		});
	}
};

const newComputed = (...args) => {
	const dependentProperties = args.slice(0, -1);
	const definition = args[args.length - 1];
	const readOnly = !definition.set;

	if (definition.set) {
		const oldSet = definition.set;
		definition.set = function(key, value) {
			oldSet.call(this, key, value);
			return definition.get.call(this, key);
		};
	}

	const property = Ember.computed(...dependentProperties, definition);

	if (readOnly) {
		return property.readOnly();
	} else {
		return property;
	}
};

const computed = (isNewVersion ? newComputed : oldComputed);

export { computed };

<|start_filename|>test/attribute_type/boolean.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.BooleanType.create();

	module('Boolean Attribute Type Test');

	test('Serialization works correctly', function() {
		expect(12);

		strictEqual(type.serialize(true), true);
		strictEqual(type.serialize('true'), true);
		strictEqual(type.serialize(new Boolean(true)), true); // eslint-disable-line no-new-wrappers
		strictEqual(type.serialize(Boolean(true)), true);

		strictEqual(type.serialize(false), false);
		strictEqual(type.serialize('false'), false);
		strictEqual(type.serialize(new Boolean(false)), false); // eslint-disable-line no-new-wrappers
		strictEqual(type.serialize(Boolean(false)), false);

		strictEqual(type.serialize(''), false);
		strictEqual(type.serialize(), false);
		strictEqual(type.serialize(null), false);
		strictEqual(type.serialize(5), false);
	});

	test('Deserialization works correctly', function() {
		expect(9);

		strictEqual(type.deserialize(undefined), false);
		strictEqual(type.deserialize(null), false);
		strictEqual(type.deserialize(42), false);
		strictEqual(type.deserialize(false), false);
		strictEqual(type.deserialize(true), true);
		strictEqual(type.deserialize('true'), true);
		strictEqual(type.deserialize('true_'), false);
		strictEqual(type.deserialize({}), false);
		strictEqual(type.deserialize([]), false);
	});
})();

<|start_filename|>src/store/record_cache.js<|end_filename|>
import Ember from 'ember';

import { PromiseObject } from 'ember-graph/data/promise_object';
import { computed } from 'ember-graph/util/computed';


export default Ember.Object.extend({

	cacheTimeout: computed('_cacheTimeout', {
		get() {
			return this.get('_cacheTimeout');
		},
		set(key, value) {
			this.set('_cacheTimeout', typeof value === 'number' ? value : Infinity);
		}
	}),

	records: {},

	liveRecordArrays: {},

	init() {
		this.setProperties({
			_cacheTimeout: Infinity,
			records: {},
			liveRecordArrays: {}
		});
	},

	getRecord(typeKey, id) {
		const key = `${typeKey}:${id}`;
		const records = this.get('records');

		if (records[key] && records[key].timestamp >= (new Date()).getTime() - this.get('cacheTimeout')) {
			return records[key].record;
		}

		return null;
	},

	getRecords(typeKey) {
		const records = this.get('records');
		const found = [];
		const cutoff = (new Date()).getTime() - this.get('cacheTimeout');

		Object.keys(records).forEach((key) => {
			if (key.indexOf(typeKey) === 0 && records[key].timestamp >= cutoff) {
				found.push(records[key].record);
			}
		});

		return found;
	},

	storeRecord(record) {
		if (PromiseObject.detectInstance(record)) {
			record = record.getModel();
		}

		const typeKey = record.get('typeKey');

		const records = this.get('records');
		records[`${typeKey}:${record.get('id')}`] = {
			record,
			timestamp: (new Date()).getTime()
		};

		const liveRecordArrays = this.get('liveRecordArrays');
		liveRecordArrays[typeKey] = liveRecordArrays[typeKey] || Ember.A();
		if (!liveRecordArrays[typeKey].includes(record)) {
			liveRecordArrays[typeKey].addObject(record);
		}
	},

	deleteRecord(typeKey, id) {
		const records = this.get('records');
		delete records[`${typeKey}:${id}`];
	},

	getLiveRecordArray(typeKey) {
		const liveRecordArrays = this.get('liveRecordArrays');
		liveRecordArrays[typeKey] = liveRecordArrays[typeKey] || Ember.A();
		return liveRecordArrays[typeKey];
	}

});


<|start_filename|>test/util/initialization.js<|end_filename|>
(function() {
	'use strict';

	var App;

	module('Initializer Test', {
		teardown: function() {
			App.destroy();
		}
	});

	asyncTest('Store is on application instance', 1, function() {
		Ember.onLoad('Ember.Application', function(Application) {
			App = Application.create({
				rootElement: '#test-app',
				ready: function() {
					start();
					ok(App.store instanceof EmberGraph.Store);
				}
			});
		});
	});

	asyncTest('Store is available as store:main', 1, function() {
		Ember.onLoad('Ember.Application', function(Application) {
			App = Application.create({
				rootElement: '#test-app',
				ready: function() {
					start();

					var container = this.__container__;
					ok(container.lookup('store:main') instanceof EmberGraph.Store);
				}
			});
		});
	});

	if (Ember.Service) {
		asyncTest('Store is available as service:store', 1, function() {
			Ember.onLoad('Ember.Application', function(Application) {
				App = Application.create({
					rootElement: '#test-app',
					ready: function() {
						start();

						var container = this.__container__;
						ok(container.lookup('service:store') instanceof EmberGraph.Store);
					}
				});
			});
		});
	}
})();


<|start_filename|>src/model/schema.js<|end_filename|>
import Ember from 'ember';
import Model from 'ember-graph/model/model';

/**
 * Declares an attribute on a model. The options determine the type and behavior
 * of the attributes. Bold options are required:
 *
 * - **`type`**: The type of the attribute. `string`, `boolean`, `number`, `date`, `array`
 * and `object` are the built in types. New types can be declared by extending `AttributeType`.
 * - `defaultValue`: The value that gets used if the attribute is missing from the loaded data.
 * This can be a function if the value needs to be computed or you need to return different
 * instances of an object each time.
 * If omitted, the attribute is required and will error if missing.
 * - `readOnly`: Set to `true` to make the attribute read-only (except for new records). Defaults to `false`.
 * - `isEqual`: Function that will compare two different instances of the attribute. Should take
 * two arguments and return `true` if the given attributes are equal. Defaults to the function
 * declared in the `AttributeType` subclass. **deprecated**
 * - `serverOnly`: This marks the attribute as a server-only attribute. This can be used when
 * an attribute is required for the model, but cannot be created on the client side. Ember-Graph
 * will allow the attribute to remain uninitialized until the record is persisted to the server.
 * This automatically makes the attribute read only.
 *
 * The option values are all available as property metadata, as well the `isAttribute` property
 * which is always `true`, and the `isRequired` property. However, the `defaultValue` property
 * should not be used directly; use the `getDefaultValue()` method instead.
 *
 * Like other Ember properties, `undefined` is _not_ a valid attribute value.
 *
 * As a shorthand, the `options` parameter may be a single string type. So this
 *
 * ```js
 * EmberGraph.attr({
 *     type: 'string'
 * })
 * ```
 *
 * can be turned into this
 *
 * ```js
 * EmberGraph.attr('string')
 * ```
 *
 * @method attr
 * @param {Object} options
 * @return {Object} Property descriptor used by model during initialization
 * @namespace EmberGraph
 */
var attr = function(options) {
	let optionsObject = options;

	if (Ember.typeOf(optionsObject) === 'string') {
		optionsObject = { type: optionsObject };
	}

	Ember.deprecate('The `isEqual` method on attributes is deprecated. ' +
			'Please use a custom attribute type instead.', !optionsObject.isEqual, { id: 'isEqual', until: '2.0.0' });

	return {
		isAttribute: true,
		options: optionsObject
	};
};

/**
 * Declares a *-to-many relationship on a model. The options determine
 * the type and behavior of the relationship. Bold options are required:
 *
 * - **`relatedType`**: The type of the related models.
 * - **`inverse`**: The relationship on the related models that reciprocates this relationship.
 * - `isRequired`: `false` if the relationship can be left out of the JSON. Defaults to `true`.
 * - `defaultValue`: The value that gets used if the relationship is missing from the loaded data.
 * The default is an empty array. This can be a function if the value needs to be computed or
 * you need to return different instances of an object each time.
 * - `readOnly`: Set to `true` to make the relationship read-only (except for new records). Defaults to `false`.
 * - `serverOnly`: This marks the relationship as a server-only relationship. This can be used when
 * a relationship is required for the model, but cannot be created on the client side. Ember-Graph
 * will allow the relationship to remain uninitialized until the record is persisted to the server.
 * This automatically makes the relationship read only.
 *
 * The option values are all available as property metadata, as well the `isRelationship` property
 * which is always `true`, and the `kind` property which is always `hasMany`. However, the
 * `defaultValue` property should not be used directly; use the `getDefaultValue()` method instead.
 *
 * @method hasMany
 * @param {Object} options
 * @return {Object} Property descriptor used by model during initialization
 * @namespace EmberGraph
 */
var hasMany = function(options) {
	return {
		isRelationship: true,
		kind: Model.HAS_MANY_KEY,
		options: options
	};
};

/**
 * Declares a *-to-one relationship on a model. The options determine
 * the type and behavior of the relationship. Bold options are required:
 *
 * - **`relatedType`**: The type of the related models.
 * - **`inverse`**: The relationship on the related model that reciprocates this relationship.
 * - `isRequired`: `false` if the relationship can be left out of the JSON. Defaults to `true`.
 * - `defaultValue`: The value that gets used if the relationship is missing from the loaded data.
 * The default is `null`. This can be a function if the value needs to be computed or you need
 * to return different instances of an object each time.
 * - `readOnly`: Set to `true` to make the relationship read-only (except for new records). Defaults to `false`.
 * - `serverOnly`: This marks the relationship as a server-only relationship. This can be used when
 * a relationship is required for the model, but cannot be created on the client side. Ember-Graph
 * will allow the relationship to remain uninitialized until the record is persisted to the server.
 * This automatically makes the relationship read only.
 *
 * The option values are all available as property metadata, as well the `isRelationship` property
 * which is always `true`, and the `kind` property which is always `hasOne`. However, the
 * `defaultValue` property should not be used directly; use the `getDefaultValue()` method instead.
 *
 * @method hasOne
 * @param {Object} options
 * @return {Object} Property descriptor used by model during initialization
 * @namespace EmberGraph
 */
var hasOne = function(options) {
	return {
		isRelationship: true,
		kind: Model.HAS_ONE_KEY,
		options: options
	};
};

export {
	attr,
	hasMany,
	hasOne
};


<|start_filename|>test/model/relationship.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Relationship Functionality Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: false
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: false
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: false,
						defaultValue: [{ type: 'tag', id: '0' }]
					})
				}),

				tag: EG.Model.extend()
			});

			store.pushPayload({
				user: [
					{ id: '1', posts: [{ type: 'post', id: '1' }, { type: 'post', id: '2' }] },
					{ id: '2', posts: [{ type: 'post', id: '3' }] },
					{ id: '3', posts: [{ type: 'post', id: '4' }, { type: 'post', id: '7' }] },
					{ id: '4', posts: [] }
				],
				post: [
					{ id: '1', author: { type: 'user', id: '1' }, tags: [{ type: 'tag', id: '1' },
						{ type: 'tag', id: '2' }, { type: 'tag', id: '3' }, { type: 'tag', id: '4' }] },
					{ id: '2', author: { type: 'user', id: '1' }, tags: [{ type: 'tag', id: '2' },
						{ type: 'tag', id: '3' }] },
					{ id: '3', author: { type: 'user', id: '2' }, tags: [] },
					{ id: '4', author: { type: 'user', id: '3' }, tags: [{ type: 'tag', id: '1' },
						{ type: 'tag', id: '4' }, { type: 'tag', id: '5' }] },
					{ id: '5', author: { type: 'user', id: '5' }, tags: [{ type: 'tag', id: '1' },
						{ type: 'tag', id: '4' }, { type: 'tag', id: '5' }] },
					{ id: '6', author: null, tags: [{ type: 'tag', id: '1' }, { type: 'tag', id: '2' },
						{ type: 'tag', id: '5' }] },
					// 7 is used as an unloaded record
					{ id: '8', author: null, tags: [{ type: 'tag', id: '4' }] }
				],

				tag: [
					{ id: '1' },
					{ id: '2' },
					{ id: '3' },
					{ id: '4' }
				]
			});
		}
	});

	test('Relationships are loaded correctly', function() {
		expect(10);

		var user1 = store.getRecord('user', '1');
		var user4 = store.getRecord('user', '4');
		var post1 = store.getRecord('post', '1');
		var post3 = store.getRecord('post', '3');
		var post5 = store.getRecord('post', '5');
		var post6 = store.getRecord('post', '6');

		deepEqual(user1.get('_posts').mapBy('id').sort(), ['1', '2'].sort());
		strictEqual(user4.get('_posts.length'), 0);

		strictEqual(post1.get('_author').id, '1');
		deepEqual(post1.get('_tags').mapBy('id').sort(), ['1', '2', '3', '4'].sort());
		strictEqual(post3.get('_author').id, '2');
		strictEqual(post3.get('_tags.length'), 0);
		strictEqual(post5.get('_author').id, '5');
		deepEqual(post5.get('_tags').mapBy('id').sort(), ['1', '4', '5'].sort());
		strictEqual(post6.get('_author'), null);
		deepEqual(post6.get('_tags').mapBy('id').sort(), ['1', '2', '5'].sort());
	});

	test('When a record is loaded, its pending relationships are attached', function() {
		expect(5);

		var user3 = store.getRecord('user', '3');
		ok(user3.get('_posts').mapBy('id').indexOf('7') >= 0);

		var rid;
		var afterRid;
		var relationship;
		var afterRelationship;
		var queued = store.get('queuedRelationships');

		var current = queued.buckets['7'];
		while (current) {
			if ((current.item.get('type2') === 'post') && (current.item.get('id2') === '7')) {
				relationship = current.item;
				rid = relationship.get('id');
				break;
			}
		}

		store.pushPayload({
			post: [{
				id: '7',
				author: { type: 'user', id: '3' },
				tags: []
			}]
		});

		current = queued.buckets['7'];
		while (current) {
			if ((current.item.get('type2') === 'post') && (current.item.get('id2') === '7')) {
				afterRelationship = current.item;
				afterRid = afterRelationship.get('id');
				break;
			}
		}

		var post = store.getRecord('post', '7');

		strictEqual(typeof rid, 'string');
		strictEqual(afterRid, undefined);
		ok(user3.get('_posts').mapBy('id').indexOf('7') >= 0);
		strictEqual(post.get('_author').id, '3');
	});

	test('A new record attaches to current records correctly', function() {
		expect(3);

		var user = store.getRecord('user', '1');
		var post = store.createRecord('post',
			{ author: { type: 'user', id: '1' }, tags: [{ type: 'tag', id: '1' }, { type: 'tag', id: '2' }] });

		strictEqual(post.get('_author').id, '1');
		deepEqual(post.get('_tags').mapBy('id').sort(), ['1', '2'].sort());
		ok(user.get('_posts').mapBy('id').indexOf(post.get('id')) >= 0);
	});

	test('Removing from a hasMany saved to the server works', function() {
		expect(2);

		var post1 = store.getRecord('post', '1');
		deepEqual(post1.get('_tags').mapBy('id').sort(), ['1', '2', '3', '4'].sort());

		post1.removeFromRelationship('tags', '2');
		deepEqual(post1.get('_tags').mapBy('id').sort(), ['1', '3', '4'].sort());
	});

	test('Removing a non existent hasMany item has no effect', function() {
		expect(1);

		var user1 = store.getRecord('user', '1');
		var current = user1.get('_posts').mapBy('id').sort();

		user1.removeFromRelationship('posts', '298133');
		deepEqual(user1.get('_posts').mapBy('id').sort(), current);
	});

	test('Disconnecting a hasOne saved to the server works', function() {
		expect(4);

		var user1 = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');

		ok(user1.get('_posts').mapBy('id').indexOf('1') >= 0);
		strictEqual(post1.get('_author').id, '1');

		post1.clearHasOneRelationship('author');

		strictEqual(post1.get('_author'), null);
		ok(user1.get('_posts').mapBy('id').indexOf('1') < 0);
	});

	test('Disconnecting a null hasOne has no effect', function() {
		expect(2);

		var post6 = store.getRecord('post', '6');
		strictEqual(post6.get('_author'), null);
		post6.clearHasOneRelationship('author');
		strictEqual(post6.get('_author'), null);
	});

	test('Changing a hasOne from one record to another works', function() {
		expect(6);

		var user1 = store.getRecord('user', '1');
		var user2 = store.getRecord('user', '2');
		var post1 = store.getRecord('post', '1');

		ok(user1.get('_posts').mapBy('id').indexOf('1') >= 0);
		ok(user2.get('_posts').mapBy('id').indexOf('1') < 0);
		strictEqual(post1.get('_author').id, '1');

		post1.setHasOneRelationship('author', '2');

		ok(user1.get('_posts').mapBy('id').indexOf('1') < 0);
		ok(user2.get('_posts').mapBy('id').indexOf('1') >= 0);
		strictEqual(post1.get('_author').id, '2');
	});

	test('Rolling back a record with no changes has no effect', function() {
		expect(2);

		var post1 = store.getRecord('post', '1');
		var author = post1.get('_author');
		var tags = post1.get('_tags');

		post1.rollbackRelationships();

		deepEqual(post1.get('_author').id, author.id);
		deepEqual(post1.get('_tags').mapBy('id').sort(), tags.mapBy('id').sort());
	});

	test('Adding to a hasMany works properly', function() {
		expect(2);

		var post2 = store.getRecord('post', '2');
		ok(post2.get('_tags').mapBy('id').indexOf('1') < 0);
		post2.addToRelationship('tags', '1');
		ok(post2.get('_tags').mapBy('id').indexOf('1') >= 0);
	});

	test('Setting a hasOne works properly', function() {
		expect(4);

		var user1 = store.getRecord('user', '1');
		var post6 = store.getRecord('post', '6');

		ok(user1.get('_posts').mapBy('id').indexOf('6') < 0);
		strictEqual(post6.get('_author'), null);

		post6.setHasOneRelationship('author', '1');

		ok(user1.get('_posts').mapBy('id').indexOf('6') >= 0);
		strictEqual(post6.get('_author').id, '1');
	});

	test('Removing an item from a hasMany dirties both records', function() {
		expect(4);

		var user1 = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');

		ok(!user1.get('isDirty'));
		ok(!post1.get('isDirty'));

		user1.removeFromRelationship('posts', '1');

		ok(user1.get('isDirty'));
		ok(post1.get('isDirty'));
	});

	test('Adding an item to a hasMany dirties both records', function() {
		expect(4);

		var user1 = store.getRecord('user', '1');
		var post6 = store.getRecord('post', '6');

		ok(!user1.get('isDirty'));
		ok(!post6.get('isDirty'));

		user1.addToRelationship('posts', '6');

		ok(user1.get('isDirty'));
		ok(post6.get('isDirty'));
	});

	test('Clearing a hasOne dirties both records', function() {
		expect(4);

		var user1 = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');

		ok(!user1.get('isDirty'));
		ok(!post1.get('isDirty'));

		post1.clearHasOneRelationship('author');

		ok(user1.get('isDirty'));
		ok(post1.get('isDirty'));
	});

	test('Setting a hasOne dirties all three records (if applicable)', function() {
		expect(6);

		var user1 = store.getRecord('user', '1');
		var user2 = store.getRecord('user', '2');
		var post1 = store.getRecord('post', '1');

		ok(!user1.get('isDirty'));
		ok(!user2.get('isDirty'));
		ok(!post1.get('isDirty'));

		post1.setHasOneRelationship('author', '2');

		ok(user1.get('isDirty'));
		ok(user2.get('isDirty'));
		ok(post1.get('isDirty'));
	});

	test('Adding back a removed item to a hasMany cleans the record', function() {
		expect(3);

		var user1 = store.getRecord('user', '1');

		ok(!user1.get('isDirty'));
		user1.removeFromRelationship('posts', '1');
		ok(user1.get('isDirty'));
		user1.addToRelationship('posts', '1');
		ok(!user1.get('isDirty'));
	});

	test('Removing an added item from a hasMany cleans the record', function() {
		expect(3);

		var user1 = store.getRecord('user', '1');

		ok(!user1.get('isDirty'));
		user1.addToRelationship('posts', '6');
		ok(user1.get('isDirty'));
		user1.removeFromRelationship('posts', '6');
		ok(!user1.get('isDirty'));
	});

	test('Setting a cleared hasOne to the old value cleans the record', function() {
		expect(3);

		var post1 = store.getRecord('post', '1');

		ok(!post1.get('isDirty'));
		post1.clearHasOneRelationship('author');
		ok(post1.get('isDirty'));
		post1.setHasOneRelationship('author', '1');
		ok(!post1.get('isDirty'));
	});

	test('Clearing a set hasOne cleans the record', function() {
		expect(3);

		var post6 = store.getRecord('post', '6');

		ok(!post6.get('isDirty'));
		post6.setHasOneRelationship('author', '1');
		ok(post6.get('isDirty'));
		post6.clearHasOneRelationship('author');
		ok(!post6.get('isDirty'));
	});

	test('Rolling back relationship changes works', function() {
		expect(5);

		var post1 = store.getRecord('post', '1');
		var tags = post1.get('_tags');
		var author = post1.get('_author');

		ok(!post1.get('isDirty'));

		post1.clearHasOneRelationship('author');
		post1.removeFromRelationship('tags', '1');
		post1.addToRelationship('tags', '5');

		ok(post1.get('isDirty'));

		post1.rollbackRelationships();

		ok(!post1.get('isDirty'));
		deepEqual(post1.get('_tags').mapBy('id').sort(), tags.mapBy('id').sort());
		deepEqual(post1.get('_author'), author);
	});

	test('Changed relationships are detected correctly', function() {
		expect(4);

		var post = store.getRecord('post', '8');
		var tags = post.get('_tags');

		post.setHasOneRelationship('author', '1');
		post.addToRelationship('tags', '1');
		post.addToRelationship('tags', '2');
		post.removeFromRelationship('tags', '4');

		var changed = post.changedRelationships();

		strictEqual(changed.author[0], null);
		strictEqual(changed.author[1].id, '1');
		deepEqual(changed.tags[0].mapBy('id').sort(), tags.mapBy('id').sort());
		deepEqual(changed.tags[1].mapBy('id').sort(), ['1', '2'].sort());
	});

	test('Reloading a changed hasOne from the server works correctly (clean record)', function() {
		expect(2);

		var post = store.getRecord('post', '1');

		store.pushPayload({
			post: [
				{ id: '1', author: { type: 'user', id: '50' }, tags: post.get('_tags') }
			]
		});

		strictEqual(post.get('_author').id, '50');
		var user = store.getRecord('user', '1');
		ok(user.get('_posts').mapBy('id').indexOf('1') < 0);
	});

	test('Reloading a cleared hasOne from the server works correctly (clean record)', function() {
		expect(2);

		var post = store.getRecord('post', '1');

		store.pushPayload({
			post: [
				{ id: '1', author: null, tags: post.get('_tags') }
			]
		});

		strictEqual(post.get('_author'), null);
		var user = store.getRecord('user', '1');
		ok(user.get('_posts').mapBy('id').indexOf('1') < 0);
	});

	test('Reloading a changed hasMany from the server works correctly (clean record)', function() {
		expect(3);

		var user = store.getRecord('user', '1');

		store.pushPayload({
			user: [
				{ id: '1', posts: [{ type: 'post', id: '1' }, { type: 'post', id: '50' }, { type: 'post', id: '51' }] }
			]
		});

		var post1 = store.getRecord('post', '1');
		var post2 = store.getRecord('post', '2');

		deepEqual(user.get('_posts').mapBy('id').sort(), ['1', '50', '51'].sort());
		strictEqual(post1.get('_author').id, '1');
		strictEqual(post2.get('_author'), null);
	});

	test('Changing a record that isn\'t loaded yet will load changes on load', function() {
		expect(4);

		var user = store.getRecord('user', '3');
		ok(user.get('_posts').mapBy('id').indexOf('7') >= 0);
		user.removeFromRelationship('posts', '7');
		ok(user.get('_posts').mapBy('id').indexOf('7') < 0);

		store.pushPayload({
			post: [
				{ id: '7', author: { type: 'user', id: '3' }, tags: [] }
			]
		});

		var post = store.getRecord('post', '7');

		strictEqual(post.get('_author'), null);
		post.rollbackRelationships();
		strictEqual(post.get('_author').id, '3');
	});

	test('A new permanent record loaded creates new server relationships', function() {
		expect(3);

		store.pushPayload({
			post: [{
				id: '50',
				author: { type: 'user', id: '1' }
			}]
		});

		var post = store.getRecord('post', '50');
		deepEqual(post.get('_author'), { type: 'user', id: '1' });
		var user = store.getRecord('user', '1');
		ok(user.get('_posts').mapBy('id').indexOf('50') >= 0);
		user.rollbackRelationships();
		ok(user.get('_posts').mapBy('id').indexOf('50') >= 0);
	});

	asyncTest('Loading a hasOne relationship fully returns the correct record', function() {
		expect(2);

		var post = store.getRecord('post', '1');
		var user = store.getRecord('user', '1');
		var promise = post.get('author');

		start();
		ok(promise instanceof EG.PromiseObject);
		stop();

		promise.then(function(author) {
			start(0);
			ok(user === author);
		});
	});

	asyncTest('Loading a hasMany relationship fully returns the correct records', function() {
		expect(3);

		var user = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');
		var post2 = store.getRecord('post', '2');

		var promise = user.get('posts');

		start();
		ok(promise instanceof EG.PromiseArray);
		stop();

		promise.then(function(posts) {
			start(0);

			var set1 = EG.Set.create();
			set1.addObjects(posts);
			var set2 = EG.Set.create();
			set2.addObjects([post1, post2]);

			ok(posts.length === 2);
			ok(set1.isEqual(set2));
		});
	});

	test('Client relationships can override server relationships', function() {
		expect(3);

		var post = store.getRecord('post', '1');
		deepEqual(post.get('_author'), { type: 'user', id: '1' });

		var user = store.createRecord('user', { posts: [{ type: 'post', id: '1' }] });
		ok(user.get('_posts').mapBy('id').indexOf('1') >= 0);
		strictEqual(post.get('_author').id, user.get('id'));
	});

	test('A hasMany can be changed by removing the record from its inverse', function() {
		expect(2);

		var user = store.getRecord('user', '1');
		var post = store.getRecord('post', '1');
		post.clearHasOneRelationship('author');

		ok(!user.get('_posts').contains('1'));
		ok(post.get('_author') === null);
	});

	test('Changing a relationship on one record propagates to the second and third records', function() {
		expect(6);

		var user1 = store.getRecord('user', '1');
		var user2 = store.getRecord('user', '2');
		var post = store.getRecord('post', '1');

		ok(user1.get('_posts').mapBy('id').indexOf('1') >= 0);
		ok(user2.get('_posts').mapBy('id').indexOf('1') < 0);
		deepEqual(post.get('_author'), { type: 'user', id: '1' });

		user2.addToRelationship('posts', '1');

		ok(user1.get('_posts').mapBy('id').indexOf('1') < 0);
		ok(user2.get('_posts').mapBy('id').indexOf('1') >= 0);
		deepEqual(post.get('_author'), { type: 'user', id: '2' });
	});

	test('A server side relationship on a non-loaded record can be overridden by a client side one', function() {
		expect(7);

		var user1 = store.getRecord('user', '1');
		var user3 = store.getRecord('user', '3');

		ok(user1.get('_posts').mapBy('id').indexOf('7') < 0);
		ok(user3.get('_posts').mapBy('id').indexOf('7') >= 0);

		user1.addToRelationship('posts', '7');

		ok(user1.get('_posts').mapBy('id').indexOf('7') >= 0);
		ok(user3.get('_posts').mapBy('id').indexOf('7') < 0);

		store.pushPayload({
			post: [{
				id: '7',
				author: { type: 'user', id: '3' },
				tags: [{ type: 'tag', id: '1' }, { type: 'tag', id: '2' }]
			}]
		});
		var post7 = store.getRecord('post', '7');

		ok(user1.get('_posts').mapBy('id').indexOf('7') >= 0);
		ok(user3.get('_posts').mapBy('id').indexOf('7') < 0);
		deepEqual(post7.get('_author'), { type: 'user', id: '1' });
	});

	test('Observers aren\'t fired until after the relationship operations are done', function() {
		// TODO: This test seems a bit unnecessary now. Transactions should be wrapped in `changeProperties`.
		expect(3);

		var post = store.getRecord('post', '1');
		deepEqual(post.get('_author'), { type: 'user', id: '1' });

		var observer = function(obj, prop) {
			if (Em.get(obj, prop).id !== '2') {
				throw new Error();
			}
		};
		post.addObserver('_author', observer);

		Em.changeProperties(function() {
			post.setHasOneRelationship('author', '2');
			deepEqual(post.get('_author'), { type: 'user', id: '2' });
			post.removeObserver('_author', observer);
		});

		observer = function(obj, prop) {
			if (Em.get(obj, prop).id !== '1') {
				throw new Error();
			}
		};

		Em.changeProperties(function() {
			post.addObserver('_author', observer);
			post.rollbackRelationships();
			deepEqual(post.get('_author'), { type: 'user', id: '1' });
			post.removeObserver('_author', observer);
		});
	});

	test('Modifying relationships works with records and not just IDs', function() {
		expect(6);

		var user = store.getRecord('user', '1');
		var post1 = store.getRecord('post', '1');
		var post6 = store.getRecord('post', '6');
		var tag1 = store.getRecord('tag', '1');

		strictEqual(post6.get('_author'), null);
		post6.setHasOneRelationship('author', user);
		deepEqual(post6.get('_author'), { type: 'user', id: '1' });

		ok(post1.get('_tags').mapBy('id').indexOf('1') >= 0);
		post1.removeFromRelationship('tags', tag1);
		ok(post1.get('_tags').mapBy('id').indexOf('1') < 0);

		ok(post6.get('_tags').mapBy('id').indexOf('3') < 0);
		post6.addToRelationship('tags', '3');
		ok(post6.get('_tags').mapBy('id').indexOf('3') >= 0);
	});

	test('Creating records with relationships works with records and not just IDs', function() {
		expect(2);

		var user = store.getRecord('user', '1');
		var tag1 = store.getRecord('tag', '1');
		var tag3 = store.getRecord('tag', '3');

		var post = store.createRecord('post', {
			author: user,
			tags: [tag1, tag3]
		});

		deepEqual(post.get('_author'), { type: 'user', id: '1' });
		deepEqual(post.get('_tags').toArray().mapBy('id').sort(), ['1', '3'].sort());
	});

	test('Creating a new record leaves the relationships undefined', function() {
		expect(2);

		var post = store.createRecord('post');
		ok(!post.isRelationshipInitialized('author'));
		ok(!post.isRelationshipInitialized('tags'));
	});
})();


<|start_filename|>src/model/core.js<|end_filename|>
import Ember from 'ember';
import EmberGraphSet from 'ember-graph/util/set';

import { deprecateProperty } from 'ember-graph/util/util';
import { computed } from 'ember-graph/util/computed';


var createAttribute = function(attributeName, options) {
	var meta = {
		isAttribute: true,
		type: options.type,
		isRequired: !options.hasOwnProperty('defaultValue'),
		defaultValue: options.defaultValue,
		isReadOnly: options.readOnly === true || options.serverOnly === true,
		isServerOnly: options.serverOnly === true,

		getDefaultValue: function() {
			var defaultValue = this.defaultValue;
			return (typeof defaultValue === 'function' ? defaultValue() : defaultValue);
		},

		// deprecated
		isEqual: options.isEqual
	};

	return computed(`clientAttributes.${attributeName}`, `serverAttributes.${attributeName}`, {
		get(key) {
			const server = this.get(`serverAttributes.${key}`);
			const client = this.get(`clientAttributes.${key}`);
			return (client === undefined ? server : client);
		},

		set(key, value) {
			const meta = this.constructor.metaForAttribute(key);

			// New records can modify read only attributes. But not if they're server only
			if (meta.isReadOnly && !this.get('isNew')) {
				throw new Ember.Error('Cannot set read-only property "' + key + '" on object: ' + this);
			}

			if (value === undefined) {
				Ember.warn('`undefined` is not a valid property value.', false, { id: 'undefined-invalid-prop' });
				return;
			}

			const isEqualScope = meta.isEqual ? meta : this.get('store').attributeTypeFor(meta.type);

			if (isEqualScope.isEqual(this.get(`serverAttributes.${key}`), value)) {
				delete this.get('clientAttributes')[key];
			} else {
				this.set(`clientAttributes.${key}`, value);
			}

			// This only notifies observers of the object itself, not the properties.
			// At this point in time, that's only the `_areAttributesDirty` property.
			this.notifyPropertyChange('clientAttributes');
		}
	}).meta(meta);
};

/**
 * This class serves as the base for Models and Embedded records.
 * This is considered private API and shouldn't be extended
 * unless you really know what you're doing.
 *
 * @class CoreModel
 * @abstract
 */
var CoreModel = Ember.Object.extend({

	/**
	 * The latest attributes from the server. When rolling back attributes,
	 * these values will be the new current values. These should only be
	 * updated when new data is received from the server, usually as the
	 * result of a save request or an asynchronous data push.
	 *
	 * @property serverAttributes
	 * @type Object
	 * @private
	 */
	serverAttributes: null,

	/**
	 * Client side changes to attributes, if there are any. These values
	 * are temporary and do not take effect until persisted to the server
	 * and turned into server attributes by being pushed with a payload.
	 * When rolling back attributes, this object is replaced with an
	 * empty one.
	 *
	 * @property clientAttributes
	 * @type Object
	 * @private
	 */
	clientAttributes: null,

	/**
	 * Determines if there are any dirty attributes.
	 *
	 * @property areAttributesDirty
	 * @type Boolean
	 */
	areAttributesDirty: computed('clientAttributes', {
		get() {
			return Object.keys(this.get('clientAttributes') || {}).length > 0;
		}
	}),

	_areAttributesDirty: deprecateProperty('`_areAttributeDirty` is now `areAttributesDirty`', 'areAttributesDirty'),

	init: function() {
		this._super();

		this.setProperties({
			serverAttributes: Ember.Object.create(),
			clientAttributes: Ember.Object.create()
		});
	},

	/**
	 * Returns an object that contains every attribute
	 * that has been changed since the last save.
	 *
	 * @method changedAttributes
	 * @return {Object} Keys are attribute names, values are arrays with [oldVal, newVal]
	 */
	changedAttributes: function() {
		var diff = {};
		var store = this.get('store');

		this.constructor.eachAttribute(function(name, meta) {
			var server = this.get('serverAttributes.' + name);
			var client = this.get('clientAttributes.' + name);

			var scope = meta.isEqual ? meta : store.attributeTypeFor(meta.type);
			if (client === undefined || scope.isEqual(server, client)) {
				return;
			}

			diff[name] = [server, client];
		}, this);

		return diff;
	},

	/**
	 * Resets all attribute changes to last known server attributes.
	 *
	 * @method rollbackAttributes
	 */
	rollbackAttributes: function() {
		this.set('clientAttributes', Ember.Object.create());
	},

	/**
	 * Resets a single attribute to last known server attribute
	 *
	 * @method rollbackSingleAttribute
	 * @param {String} attr
	 */
	rollbackSingleAttribute: function(attr) {
		this.clientAttributes.set(attr, this.serverAttributes.get(attr));
	},

	/**
	 * Loads attributes from the server.
	 */
	loadAttributesFromServer: function(json) {
		var serverAttributes = this.get('serverAttributes');

		this.constructor.eachAttribute(function(attributeName, meta) {
			Ember.assert('Your JSON is missing the \'' + attributeName + '\' property.',
				!meta.isRequired || json.hasOwnProperty(attributeName));

			// TODO: Why is there here? I thought we weren't allowing this.
			var value = (json.hasOwnProperty(attributeName) ? json[attributeName] : meta.getDefaultValue());
			serverAttributes.set(attributeName, value);

			this.synchronizeAttribute(attributeName);
		}, this);
	},

	/**
	 * When an attribute's value is set directly (like in `pushPayload`), this
	 * will synchronize the server and client attributes and fix the dirty state.
	 */
	synchronizeAttribute: function(name) {
		var server = this.get('serverAttributes.' + name);
		var client = this.get('clientAttributes.' + name);

		var meta = this.constructor.metaForAttribute(name);
		var scope = meta.isEqual ? meta : this.get('store').attributeTypeFor(meta.type);

		if (scope.isEqual(server, client)) {
			delete this.get('clientAttributes')[name];
			this.notifyPropertyChange('clientAttributes');
		}
	},

	/**
	 * Sets up attributes given to the constructor for this record.
	 * Equivalent to setting the attribute values individually.
	 */
	initializeAttributes: function(json) {
		this.constructor.eachAttribute(function(name, meta) {
			var value = json[name];

			if (value !== undefined) {
				this.set(name, value);
			}
		}, this);
	},

	areAttributesInitialized: function() {
		var initialized = true;

		this.constructor.eachAttribute(function(name, meta) {
			if (meta.isRequired && !meta.isServerOnly) {
				initialized = initialized && this.isAttributeInitialized(name);
			}
		}, this);

		return initialized;
	},

	/**
	 * Determines if the given attribute has been initialized or not.
	 * Always returns `true` for non-new records.
	 *
	 * @method isAttributeInitialized
	 * @param attributeName
	 * @return {Boolean}
	 */
	isAttributeInitialized: function(attributeName) {
		return !this.get('isNew') || this.get(attributeName) !== undefined;
	}

});

CoreModel.reopenClass({

	/**
	 * At extend time, this method goes though and declares properties
	 * on the class for all attributes that were declared. It's done
	 * this way so the name of the attributes is known when creating
	 * the computed property.
	 *
	 * @method declareAttributes
	 * @param {Object} attributes
	 * @private
	 * @static
	 */
	declareAttributes: function(attributes) {
		var obj = {};

		Ember.runInDebug(function() {
			var RESERVED_NAMES = EmberGraphSet.create();
			RESERVED_NAMES.addObjects(['id', 'type', 'content', 'length', 'model']);

			Object.keys(attributes).forEach(function(name) {
				Ember.assert('`' + name + '` cannot be used as an attribute name.', !RESERVED_NAMES.contains(name));
				Ember.assert('An attribute name cannot start with an underscore.', name.charAt(0) !== '_');
				Ember.assert('Attribute names must start with a lowercase letter.', name.charAt(0).match(/[a-z]/));
			});
		});

		Object.keys(attributes).forEach(function(name) {
			obj[name] = createAttribute(name, attributes[name].options);
		});

		this.reopen(obj);
	},

	/**
	 * A set of all of the attribute names for this model.
	 *
	 * @property attributes
	 * @type Set
	 * @static
	 * @readOnly
	 */
	attributes: computed({
		get() {
			const attributes = EmberGraphSet.create();

			this.eachComputedProperty(function(name, meta) {
				if (meta.isAttribute) {
					attributes.addObject(name);
				}
			});

			return attributes;
		}
	}),

	/**
	 * Returns the metadata for the given property name. This should
	 * always be used over `metaForProperty` just in case the
	 * implementations ever have to differ.
	 *
	 * @method metaForAttribute
	 * @param {String} attributeName
	 * @return {Object}
	 * @static
	 */
	metaForAttribute() {
		return this.metaForProperty(...arguments);
	},

	/**
	 * @method isAttribute
	 * @param {String} propertyName
	 * @return {Boolean}
	 * @static
	 */
	isAttribute: function(propertyName) {
		return Ember.get(this, 'attributes').contains(propertyName);
	},

	/**
	 * Calls the callback for each attribute defined on the model.
	 *
	 * @method eachAttribute
	 * @param {Function} callback Function that takes `name` and `meta` parameters
	 * @param [binding] Object to use as `this`
	 * @static
	 */
	eachAttribute: function(callback, binding) {
		this.eachComputedProperty(function(name, meta) {
			if (meta.isAttribute) {
				callback.call(binding, name, meta);
			}
		});
	}

});

export default CoreModel;


<|start_filename|>tasks/options/eslint.js<|end_filename|>
module.exports = {
	source: ['src/**/*.js'],
	tests: ['test/**/*.js'],
	tasks: ['Gruntfile.js', 'tasks/**/*.js']
};

<|start_filename|>test/data/promise_object.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Promise Object Tests', {
		setup: function() {
			store = setupStore({
				tag: EG.Model.extend({
					name: EG.attr({
						type: 'string'
					})
				})
			});

			store.pushPayload({
				tag: [
					{ id: '1', name: 'tag1' },
					{ id: '2', name: 'tag2' },
					{ id: '3', name: 'tag3' },
					{ id: '4', name: 'tag4' }
				]
			});
		}
	});

	asyncTest('Model promise object correctly reports the ID and typeKey', function() {
		expect(6);

		var resolve = null;
		var modelPromise = EG.ModelPromiseObject.create({
			id: '1',
			typeKey: 'tag',
			promise: new Em.RSVP.Promise(function(r) {
				resolve = r;
			})
		});

		start();
		strictEqual(modelPromise.get('id'), '1');
		strictEqual(modelPromise.get('typeKey'), 'tag');
		strictEqual(modelPromise.get('name'), undefined);
		stop();

		resolve(store.getRecord('tag', '1'));

		modelPromise.then(function() {
			start();
			strictEqual(modelPromise.get('id'), '1');
			strictEqual(modelPromise.get('typeKey'), 'tag');
			strictEqual(modelPromise.get('name'), 'tag1');
		});
	});
})();



<|start_filename|>test/model/relationship/creating.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Model Creation Relationship Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author'
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts'
					})
				})
			}, {
				adapter: EG.Adapter.extend({
					createRecord: function(record) {
						return Em.RSVP.resolve({
							meta: {
								createdRecord: {
									type: record.get('typeKey'),
									id: 'NEW_POST_ID'
								}
							},
							post: [{
								id: 'NEW_POST_ID',
								author: { type: 'user', id: '1' }
							}]
						});
					}
				})
			});

			store.pushPayload({
				user: [{
					id: '1',
					posts: []
				}]
			});
		}
	});

	test('Saving a new record updates the relationships with the new ID', function() {
		expect(1);

		var post = store.createRecord('post', { author: '1' });
		var tempId = post.get('id');

		var all = store.get('allRelationships');
		Object.keys(all).forEach(function(id) {
			if (all[id].get('id1') === tempId || all[id].get('id2') === tempId) {
				ok(true);
			}
		});

		post.save().then(function() {
			var all = store.get('allRelationships');

			Object.keys(all).forEach(function(id) {
				if (all[id].get('id1') === tempId || all[id].get('id2') === tempId) {
					ok(false);
				}
			});
		});
	});
})();

<|start_filename|>src/after_load.js<|end_filename|>
/* global define require */

var configureAliases = function() {
	var configureAlias = function(original, alias) {
		define(alias, ['exports', original], function(exports, original) {
			for (var key in original) {
				if (original.hasOwnProperty(key)) {
					exports[key] = original[key];
				}
			}
		});
	};

	configureAlias('ember-graph/main', 'ember-graph');
	configureAlias('ember-graph/util/util', 'ember-graph/util');
	configureAlias('ember-graph/serializer/serializer', 'ember-graph/serializer');
	configureAlias('ember-graph/adapter/adapter', 'ember-graph/adapter');
	configureAlias('ember-graph/adapter/ember_graph/adapter', 'ember-graph/adapter/ember_graph');
	configureAlias('ember-graph/model/model', 'ember-graph/model');
	configureAlias('ember-graph/attribute_type/type', 'ember-graph/attribute_type');
	configureAlias('ember-graph/store/store', 'ember-graph/store');
};

require(['ember-graph/initializer']);
require(['ember-graph/shim']);

configureAliases();

<|start_filename|>src/attribute_type/enum.js<|end_filename|>
import Ember from 'ember';
import EmberGraphSet from 'ember-graph/util/set';
import AttributeType from 'ember-graph/attribute_type/type';

import { computed } from 'ember-graph/util/computed';


/**
 * Represents an enumeration or multiple choice type. This class cannot be
 * instantiated directly, you must extend the class, overriding both the
 * `defaultValue` and `values` properties. The `values` property must be
 * an array of unique strings (case insensitive). The `defaultValue` must
 * be a string, and the value must also exist in the `values` array.
 *
 * @class EnumType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * The default enum value. Must be overridden in subclasses.
	 *
	 * @property defaultValue
	 * @type String
	 * @final
	 */
	defaultValue: computed({
		get() {
			throw new Ember.Error('You must override the `defaultValue` in an enumeration type.');
		}
	}),

	/**
	 * @property values
	 * @type {Array<String>}
	 * @default []
	 * @final
	 */
	values: [],

	/**
	 * Contains all of the values converted to lower case.
	 *
	 * @property valueSet
	 * @type {Set<String>}
	 * @default []
	 * @final
	 */
	valueSet: computed('values', {
		get() {
			const set = EmberGraphSet.create();
			const values = this.get('values');

			set.addObjects(values.map((value) => value.toLocaleLowerCase()));

			return set;
		}
	}),

	/**
	 * Determines if the given option is a valid enum value.
	 *
	 * @property isValidValue
	 * @param {String} option
	 * @return {Boolean}
	 */
	isValidValue: function(option) {
		return this.get('valueSet').contains(option.toLowerCase());
	},

	/**
	 * Converts the given option to a valid enum value.
	 * If the given value isn't valid, it uses the default value.
	 *
	 * @method serialize
	 * @param {String} option
	 * @return {String}
	 */
	serialize: function(option) {
		const optionString = option + '';

		if (this.isValidValue(optionString)) {
			return optionString;
		} else {
			var defaultValue = this.get('defaultValue');

			if (this.isValidValue(defaultValue)) {
				return defaultValue;
			} else {
				throw new Ember.Error('The default value you provided isn\'t a valid value.');
			}
		}
	},

	/**
	 *
	 * Converts the given option to a valid enum value.
	 * If the given value isn't valid, it uses the default value.
	 *
	 * @method deserialize
	 * @param {String} option
	 * @return {String}
	 */
	deserialize() {
		return this.serialize(...arguments);
	},

	/**
	 * Compares two enum values, case-insensitive.
	 * @param {String} a
	 * @param {String} b
	 * @return {Boolean}
	 */
	isEqual: function(a, b) {
		if (Ember.typeOf(a) !== 'string' || Ember.typeOf(b) !== 'string') {
			return false;
		} else {
			return ((a + '').toLocaleLowerCase() === (b + '').toLocaleLowerCase());
		}
	}
});


<|start_filename|>src/model/states.js<|end_filename|>
import Ember from 'ember';

import { computed } from 'ember-graph/util/computed';
import { startsWith } from 'ember-graph/util/string';

export default {

	/**
	 * Denotes that the record is currently being deleted, but the server hasn't responded yet.
	 *
	 * @property isDeleting
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isDeleting: false,

	/**
	 * Denotes that a record has been deleted and the change persisted to the server.
	 *
	 * @property isDeleted
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isDeleted: false,

	/**
	 * Denotes that the record is currently saving its changes to the server, but the server hasn't responded yet.
	 * (This doesn't overlap with `isCreating` at all. This is only true on subsequent saves.)
	 *
	 * @property isSaving
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isSaving: false,

	/**
	 * Denotes that the record is being reloaded from the server, but the server hasn't responded yet.
	 *
	 * @property isReloading
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isReloading: false,

	/**
	 * Denotes that a record has been loaded into a store and isn't freestanding.
	 *
	 * @property isLoaded
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isLoaded: computed('store', {
		get() {
			return (this.get('store') !== null);
		}
	}),

	/**
	 * Denotes that the record has attribute or relationship changes that have not been saved to the server yet.
	 * Note: A new record is always dirty.
	 *
	 * @property isDirty
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isDirty: Ember.computed.or('areAttributesDirty', 'areRelationshipsDirty', 'isNew'),

	/**
	 * Denotes that the record is currently being saved to the server for the first time,
	 * and the server hasn't responded yet.
	 *
	 * @property isCreating
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isCreating: false,

	/**
	 * Denotes that a record has just been created and has not been saved to
	 * the server yet. Most likely has a temporary ID if this is true.
	 *
	 * @property isNew
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isNew: computed('_id', {
		get() {
			return startsWith(this.get('_id'), this.constructor.temporaryIdPrefix);
		}
	}),

	/**
	 * Denotes that the record is currently waiting for the server to respond to an operation.
	 *
	 * @property isInTransit
	 * @type Boolean
	 * @final
	 * @for Model
	 */
	isInTransit: Ember.computed.or('isSaving', 'isDeleting', 'isCreating', 'isReloading')
};

<|start_filename|>test/attribute_type/string.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.StringType.create();

	module('String Attribute Type Test');

	test('Serialization works correctly', function() {
		expect(7);

		strictEqual('foo', type.serialize('foo'));
		strictEqual('bar', type.serialize(new String('bar'))); // eslint-disable-line no-new-wrappers
		strictEqual(null, type.serialize(null));
		strictEqual(null, type.serialize());
		strictEqual({}.toString(), type.serialize({}));
		strictEqual('4', type.serialize(4));
		strictEqual('true', type.serialize(true));
	});

	test('Deserialization works correctly', function() {
		expect(7);

		strictEqual(null, type.deserialize(undefined));
		strictEqual(null, type.deserialize(null));
		strictEqual('42', type.deserialize(42));
		strictEqual('false', type.deserialize(false));
		strictEqual('500', type.deserialize('500'));
		strictEqual({}.toString(), type.deserialize({}));
		strictEqual([].toString(), type.deserialize([]));
	});
})();

<|start_filename|>tasks/options/qunit.js<|end_filename|>
'use strict';

module.exports = {
	options: {
		coverage: {
			src: ['dist/ember-graph.js'],
			instrumentedFiles: 'temp/',
			lcovReport: 'report/'
		}
	},

	all: ['test/*.html']
};

<|start_filename|>test/relationship/relationship.js<|end_filename|>
(function() {
	'use strict';

	var store;

	var CLIENT_STATE = EG.Relationship.CLIENT_STATE;

	module('Relationship Object Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author'
					})
				}),
				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts'
					})
				})
			});

			store.pushPayload({
				user: [{ id: '1', posts: [] }],
				post: [{ id: '1', author: null }, { id: '2', author: null }]
			});
		}
	});

	test('Relation testing', function() {
		expect(15);

		var user = store.getRecord('user', '1');
		var post = store.getRecord('post', '2');
		var other = store.getRecord('post', '1');

		var relationship = EG.Relationship.create('user', '1', 'posts', 'post', '2', 'author', CLIENT_STATE);

		strictEqual(relationship.otherType(user), 'post');
		strictEqual(relationship.otherType(post), 'user');
		strictEqual(relationship.otherId(user), '2');
		strictEqual(relationship.otherId(post), '1');
		strictEqual(relationship.otherName(user), 'author');
		strictEqual(relationship.otherName(post), 'posts');
		strictEqual(relationship.thisName(user), 'posts');
		strictEqual(relationship.thisName(post), 'author');
		strictEqual(relationship.isConnectedTo(user), true);
		strictEqual(relationship.isConnectedTo(post), true);
		strictEqual(relationship.isConnectedTo(other), false);
		strictEqual(relationship.matchesOneSide('user', '1', 'posts'), true);
		strictEqual(relationship.matchesOneSide('post', '2', 'author'), true);
		strictEqual(relationship.matchesOneSide('user', '1', 'author'), false);
		strictEqual(relationship.matchesOneSide('test', 'foo', 'bar'), false);
	});
})();


<|start_filename|>src/attribute_type/number.js<|end_filename|>
import Ember from 'ember';
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * Will coerce any type to a number (0 being the default). `null` is not a valid value.
 *
 * @class NumberType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * @property defaultValue
	 * @default 0
	 * @final
	 */
	defaultValue: 0,

	/**
	 * Coerces the given value to a number.
	 *
	 * @method serialize
	 * @param {Number} obj Javascript object
	 * @return {Number} JSON representation
	 */
	serialize: function(obj) {
		return this.coerceToNumber(obj);
	},

	/**
	 * Coerces the given value to a number.
	 *
	 * @method deserialize
	 * @param {Number} json JSON representation of object
	 * @return {Number} Javascript object
	 */
	deserialize: function(json) {
		return this.coerceToNumber(json);
	},

	/**
	 * If the object passed is a number (and not NaN), it returns
	 * the object coerced to a number primitive. If the object is
	 * a string, it attempts to parse it (again, no NaN allowed).
	 * Otherwise, the default value is returned.
	 *
	 * @method coerceToNumber
	 * @param {Any} obj
	 * @return {Number}
	 * @protected
	 */
	coerceToNumber: function(obj) {
		if (this.isValidNumber(obj)) {
			return Number(obj).valueOf();
		}

		if (Ember.typeOf(obj) === 'string') {
			var parsed = Number(obj).valueOf();
			if (this.isValidNumber(parsed)) {
				return parsed;
			}
		}

		return 0;
	},

	/**
	 * Determines if the given number is an actual number and finite.
	 *
	 * @method isValidNumber
	 * @param {Number} num
	 * @return {Boolean}
	 * @protected
	 */
	isValidNumber: function(num) {
		return (Ember.typeOf(num) === 'number' && !isNaN(num) && isFinite(num));
	}
});

<|start_filename|>src/store/relationship.js<|end_filename|>
import Ember from 'ember';
import Relationship from 'ember-graph/relationship/relationship';
import RelationshipHash from 'ember-graph/relationship/relationship_hash';

const { CLIENT_STATE, SERVER_STATE, DELETED_STATE } = Relationship;

export default {

	allRelationships: Ember.Object.create(),

	relationshipsById: RelationshipHash.create(),

	queuedRelationships: RelationshipHash.create(),

	initializeRelationships: Ember.on('init', function() {
		this.setProperties({
			allRelationships: Ember.Object.create(),
			relationshipsById: RelationshipHash.create(),
			queuedRelationships: RelationshipHash.create()
		});
	}),

	createRelationship(type1, id1, name1, type2, id2, name2, state) {
		const relationship = Relationship.create(type1, id1, name1, type2, id2, name2, state);

		const queuedRelationships = this.get('queuedRelationships');
		const record1 = this.getRecord(type1, id1);
		const record2 = this.getRecord(type2, id2);

		if (record1) {
			this.connectRelationshipTo(record1, relationship);
		}

		if (record2) {
			this.connectRelationshipTo(record2, relationship);
		}

		this.get('relationshipsById').add(relationship, [relationship.get('id1'), relationship.get('id2')]);

		if (!record1 || !record2) {
			queuedRelationships.add(relationship, [relationship.get('id1'), relationship.get('id2')]);
			this.notifyPropertyChange('queuedRelationships');
		}

		this.get('allRelationships')[relationship.get('id')] = relationship;
	},

	deleteRelationship(relationship) {
		const record1 = this.getRecord(relationship.get('type1'), relationship.get('id1'));
		const record2 = this.getRecord(relationship.get('type2'), relationship.get('id2'));

		this.disconnectRelationshipFrom(record1, relationship);
		this.disconnectRelationshipFrom(record2, relationship);

		const queuedRelationships = this.get('queuedRelationships');
		queuedRelationships.remove(relationship, [relationship.get('id1'), relationship.get('id2')]);
		this.notifyPropertyChange('queuedRelationships');

		// de-queue from hash first otherwise we have a deleted item still in hash momentarily
		this.get('relationshipsById').remove(relationship, [relationship.get('id1'), relationship.get('id2')]);
		delete this.get('allRelationships')[relationship.get('id')];

		relationship.erase();
	},

	changeRelationshipState(relationship, newState) {
		if (relationship.get('state') === newState) {
			return;
		}

		const record1 = this.getRecord(relationship.get('type1'), relationship.get('id1'));
		const record2 = this.getRecord(relationship.get('type2'), relationship.get('id2'));

		this.disconnectRelationshipFrom(record1, relationship);
		this.disconnectRelationshipFrom(record2, relationship);

		relationship.set('state', newState);

		this.connectRelationshipTo(record1, relationship);
		this.connectRelationshipTo(record2, relationship);
	},

	connectQueuedRelationships(record) {
		const queuedRelationships = this.get('queuedRelationships');
		const potential = queuedRelationships.findAllByKeys([record.get('id')]);

		Object.keys(potential).forEach((key) => {
			let relationship = potential[key];
			if (relationship.isConnectedTo(record)) {
				this.connectRelationshipTo(record, relationship);
				queuedRelationships.remove(relationship, [relationship.get('id1'), relationship.get('id2')]);
			}
		});

		this.notifyPropertyChange('queuedRelationships');
	},

	queueConnectedRelationships(record) {
		const queued = this.get('queuedRelationships');
		const server = record.get('relationships').getRelationshipsByState(SERVER_STATE);

		server.forEach((relationship) => {
			this.disconnectRelationshipFrom(record, relationship);
			queued.add(relationship, [relationship.get('id1'), relationship.get('id2')]);
		});

		this.notifyPropertyChange('queuedRelationships');
	},

	relationshipsForRecord(type, id, name) {
		const filtered = [];
		const all = this.get('relationshipsById').findAllByKeys([id]);
		Object.keys(all).forEach((key) => {
			if (all[key].matchesOneSide(type, id, name)) {
				filtered.push(all[key]);
			}
		});

		return filtered;
	},

	relationshipsForRecordNoName(type, id) {
		const filtered = [];
		const all = this.get('relationshipsById').findAllByKeys([id]);
		Object.keys(all).forEach((key) => {
			const t = all[key];
			if ((t.get('id1') === id) && (t.get('type1') === type) ||
				(t.get('id1') === id) && (t.get('type1') === type)) {
				filtered.push(all[key]);
			}
		});

		return filtered;
	},

	deleteRelationshipsForRecord(type, id) {
		Ember.changeProperties(() => {
			const all = this.get('relationshipsById').findAllByKeys([id]);
			const keys = Object.keys(all);

			keys.forEach((key) => {
				const relationship = all[key];

				if ((relationship.get('type1') === type && relationship.get('id1') === id) ||
					(relationship.get('type2') === type && relationship.get('id2') === id)) {
					this.deleteRelationship(relationship);
				}
			});
		});
	},

	/**
	 * @param {Model} record
	 * @param {Relationship} relationship
	 * @private
	 */
	connectRelationshipTo(record, relationship) {
		if (!record) {
			return;
		}

		record.get('relationships').addRelationship(relationship.thisName(record), relationship);
	},

	/**
	 * @param {Model} record
	 * @param {Relationship} relationship
	 * @private
	 */
	disconnectRelationshipFrom(record, relationship) {
		if (!record) {
			return;
		}

		record.get('relationships').removeRelationship(relationship);
	},

	/**
	 * Takes the relationships for a hasOne relationship, and sorts them in
	 * an object that is easy to manipulate. The object returned contains
	 * the following properties:
	 *
	 * - `[SERVER_STATE]` - A single relationship or `null`
	 * - `[CLIENT_STATE]` - A single relationship or `null`
	 * - `[DELETED_STATE]` - An array of relationships
	 *
	 * There are 5 valid configurations for a hasOne relationship at any
	 * given time:
	 *
	 * 1. No relationships connected
	 * 2. A single server relationship is connected
	 * 3. A single client relationship is connected
	 * 4. One or more delete relationships is connected
	 * 5. A single client relationship, along with one or more deleted relationships
	 *
	 * This function will make assertions to ensure that the relationship
	 * exists in one of these 5 states.
	 *
	 * @param {String} type
	 * @param {String} id
	 * @param {String} name
	 * @returns {Object}
	 */
	sortHasOneRelationships(type, id, name) {
		const values = {};
		const relationships = this.relationshipsForRecord(type, id, name);

		values[SERVER_STATE] = relationships.filter((relationship) => {
			return relationship.get('state') === SERVER_STATE;
		})[0] || null;

		values[DELETED_STATE] = relationships.filter((relationship) => {
			return relationship.get('state') === DELETED_STATE;
		});

		values[CLIENT_STATE] = relationships.filter((relationship) => {
			return relationship.get('state') === CLIENT_STATE;
		})[0] || null;

		Ember.runInDebug(() => {
			/* eslint-disable */
			// No relationships at all
			if (!values[SERVER_STATE] && values[DELETED_STATE].length <= 0 && !values[CLIENT_STATE]) return;
			// One server relationship, nothing else
			if (values[SERVER_STATE] && values[DELETED_STATE].length <= 0 && !values[CLIENT_STATE]) return;
			// One client relationship, nothing else
			if (!values[SERVER_STATE] && values[DELETED_STATE].length <= 0 && values[CLIENT_STATE]) return;
			// One client relationship and some deleted relationships
			if (!values[SERVER_STATE] && values[DELETED_STATE].length > 0 && values[CLIENT_STATE]) return;
			// Some deleted relationships, nothing else
			if (!values[SERVER_STATE] && values[DELETED_STATE].length > 0 && !values[CLIENT_STATE]) return;
			/* eslint-enable */

			// Everything else is invalid
			Ember.assert('Invalid hasOne relationship values for: ' + type + '.' + name);
		});

		return values;
	},

	updateRelationshipsWithNewId(typeKey, oldId, newId) {
		const relationships = this.relationshipsForRecordNoName(typeKey, oldId);

		//  Note: Not the most efficient.  Most efficient would be to embed into relationship_hash to rename internally as crawling the queue.
		Object.keys(relationships).forEach((key) => {
			let relationship = relationships[key];
			relationship.changeId(typeKey, oldId, newId);
			if (this.get('relationshipsById').remove(relationship, [oldId])) {
				this.get('relationshipsById').add(relationship, [newId]);
			}
			if (this.get('queuedRelationships').remove(relationship, [oldId])) {
				this.get('queuedRelationships').add(relationship, [newId]);
			}
		});

		this.notifyPropertyChange('allRelationships');
	}

};


<|start_filename|>test/attribute_type/date.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.DateType.create();

	module('Date Attribute Type Test');

	test('Serialization works correctly', function() {
		expect(4);

		var now = new Date();
		strictEqual(type.serialize(now), now.getTime());
		strictEqual(type.serialize(), null);
		strictEqual(type.serialize(null), null);
		strictEqual(type.serialize(832748734), 832748734);
	});

	test('Deserialization works correctly', function() {
		expect(6);

		strictEqual(type.deserialize(undefined), null);
		strictEqual(type.deserialize(null), null);
		strictEqual(type.deserialize({}), null);
		strictEqual(type.deserialize([]), null);
		ok(type.isEqual(type.deserialize('2012-01-01'), new Date('2012-01-01')));
		ok(type.isEqual(type.deserialize(12343466000), new Date(12343466000)));
	});
})();

<|start_filename|>test/store/record_retrieval_cache.js<|end_filename|>
(function() {
	'use strict';

	var Promise = Em.RSVP.Promise;

	var typeKey = 'test';
	var cache;

	module('Record Retrieval Cache Test', {
		setup: function() {
			cache = EG.RecordRequestCache.create();
		}
	});

	test('Returns null for empty cache', function() {
		expect(5);

		strictEqual(cache.getPendingRequest(typeKey), null);
		strictEqual(cache.getPendingRequest(typeKey, '10'), null);
		strictEqual(cache.getPendingRequest(typeKey, 10), null);
		strictEqual(cache.getPendingRequest(typeKey, ['1', '2', 3]), null);
		strictEqual(cache.getPendingRequest(typeKey, { foo: 'bar' }), null);
	});

	test('Single ID retrieval', function() {
		expect(1);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, 10, promise);
		strictEqual(cache.getPendingRequest(typeKey, '10'), promise);
	});

	test('Multiple ID retrieval', function() {
		expect(1);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, ['1', '2', 3], promise);
		strictEqual(cache.getPendingRequest(typeKey, [1, 2, '3']), promise);
	});

	test('All type retrieval', function() {
		expect(1);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, promise);
		strictEqual(cache.getPendingRequest(typeKey), promise);
	});

	test('Query retrieval is unimplemented', function() {
		expect(1);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, {}, promise);
		strictEqual(cache.getPendingRequest(typeKey), null);
	});

	test('Single ID chains all type request', function() {
		expect(2);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, promise);
		strictEqual(cache.getPendingRequest(typeKey, '1'), promise);
		strictEqual(cache.getPendingRequest(typeKey, 100), promise);
	});

	test('Multiple IDs chains all type request', function() {
		expect(1);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, promise);
		strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise);
	});

	test('Single ID chains multiple ID request', function() {
		expect(2);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, [1, 2, 3], promise);
		strictEqual(cache.getPendingRequest(typeKey, '1'), promise);
		strictEqual(cache.getPendingRequest(typeKey, 3), promise);
	});

	test('Multiple ID chains request with same IDs', function() {
		expect(2);

		var promise = new Promise(function() {});
		cache.savePendingRequest(typeKey, [1, 2, 3], promise);
		strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise);
		strictEqual(cache.getPendingRequest(typeKey, [3, 1, 2]), promise);
	});

	asyncTest('Single ID promise gets removed', function() {
		expect(2);

		var promise = Promise.resolve();
		cache.savePendingRequest(typeKey, '1', promise);

		start();
		strictEqual(cache.getPendingRequest(typeKey, '1'), promise);
		stop();

		Em.run.next(function() {
			start();
			strictEqual(cache.getPendingRequest(typeKey, '1'), null);
		});
	});

	asyncTest('Multiple IDs promise gets removed', function() {
		expect(4);

		var promise = Promise.resolve();
		cache.savePendingRequest(typeKey, [1, 2, 3], promise);

		start();
		strictEqual(cache.getPendingRequest(typeKey, '1'), promise);
		strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise);
		stop();

		Em.run.next(function() {
			start();
			strictEqual(cache.getPendingRequest(typeKey, '1'), null);
			strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), null);
		});
	});

	asyncTest('All type promise gets removed', function() {
		expect(6);

		var promise = Promise.resolve();
		cache.savePendingRequest(typeKey, promise);

		start();
		strictEqual(cache.getPendingRequest(typeKey), promise);
		strictEqual(cache.getPendingRequest(typeKey, '1'), promise);
		strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise);
		stop();

		Em.run.next(function() {
			start();
			strictEqual(cache.getPendingRequest(typeKey), null);
			strictEqual(cache.getPendingRequest(typeKey, '1'), null);
			strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), null);
		});
	});
})();

<|start_filename|>src/adapter/ember_graph/server.js<|end_filename|>
import Ember from 'ember';

import { pluralize } from 'ember-graph/util/inflector';
import { generateUUID } from 'ember-graph/util/util';


export default {

	/**
	 * @method serverCreateRecord
	 * @param {String} typeKey
	 * @param {JSON} json
	 * @return {Promise}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverCreateRecord: function(typeKey, json) {
		let newId = null;

		return this.getDatabase().then((db) => {
			newId = this.generateIdForRecord(typeKey, json, db);

			const modifiedDb = this.putRecordInDatabase(typeKey, newId, json[pluralize(typeKey)][0], db);
			return this.setDatabase(modifiedDb);
		}).then((db) => {
			const record = this.getRecordFromDatabase(typeKey, newId, db);
			return {
				[pluralize(typeKey)]: [record]
			};
		});
	},

	/**
	 * @method serverFindRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {Promise}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverFindRecord: function(typeKey, id) {
		var _this = this;

		return this.getDatabase().then(function(db) {
			if (Ember.get(db, 'records.' + typeKey + '.' + id)) {
				var payload = {};
				payload[pluralize(typeKey)] = [_this.getRecordFromDatabase(typeKey, id, db)];
				return payload;
			} else {
				throw { status: 404, typeKey: typeKey, id: id };
			}
		});
	},

	/**
	 * @method serverFindMany
	 * @param {String} typeKey
	 * @param {String[]} ids
	 * @return {Promise}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverFindMany: function(typeKey, ids) {
		var _this = this;

		return this.getDatabase().then(function(db) {
			var records = ids.map(function(id) {
				if (Ember.get(db, 'records.' + typeKey + '.' + id)) {
					return _this.getRecordFromDatabase(typeKey, id, db);
				} else {
					throw { status: 404, typeKey: typeKey, id: id };
				}
			});

			var payload = {};
			payload[pluralize(typeKey)] = records;
			return payload;
		});
	},

	/**
	 * @method serverFindAll
	 * @param {String} typeKey
	 * @return {Promise}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverFindAll: function(typeKey) {
		var _this = this;

		return this.getDatabase().then(function(db) {
			var records = Object.keys(db.records[typeKey] || {}).map(function(id) {
				return _this.getRecordFromDatabase(typeKey, id, db);
			});

			var payload = {};
			payload[pluralize(typeKey)] = records;
			return payload;
		});
	},

	/**
	 * @method serverUpdateRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {JSON[]} changes
	 * @return {Promise} Resolves to update record payload
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverUpdateRecord: function(typeKey, id, changes) {
		return this.getDatabase().then((db) => {
			const modifiedDb = this.applyChangesToDatabase(typeKey, id, changes, db);
			return this.setDatabase(modifiedDb);
		}).then((db) => {
			return {
				[pluralize(typeKey)]: [this.getRecordFromDatabase(typeKey, id, db)]
			};
		});
	},

	/**
	 * @method serverDeleteRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {Promise}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	serverDeleteRecord: function(typeKey, id) {
		return this.getDatabase().then((db) => {
			if (db.records[typeKey]) {
				delete db.records[typeKey][id];
			}

			db.relationships = db.relationships.filter((r) => {
				return !((r.t1 === typeKey && r.i1 === id) || (r.t2 === typeKey && r.i2 === id));
			});

			return this.setDatabase(db).then(() => {
				return {
					meta: {
						deletedRecords: [{ type: typeKey, id: id }]
					}
				};
			});
		});
	},

	/**
	 * @method generateIdForRecord
	 * @param {String} typeKey
	 * @param {JSON} json
	 * @param {JSON} db
	 * @return {String}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	generateIdForRecord: function(typeKey, json, db) {
		return generateUUID();
	}

};

<|start_filename|>test/store/record_cache.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var cache;

	module('Record Cache Test', {
		setup: function() {
			cache = EG.RecordCache.create();

			store = setupStore({
				item: EG.Model.extend()
			});

			store.pushPayload({
				item: [
					{ id: '1' },
					{ id: '2' },
					{ id: '3' },
					{ id: '4' },
					{ id: '5' }
				]
			});
		}
	});

	test('Store and fetch a record', function() {
		expect(3);

		var item1 = store.getRecord('item', '1');
		var item2 = store.getRecord('item', '2');
		var item3 = store.getRecord('item', '3');

		cache.storeRecord(item1);
		cache.storeRecord(item2);
		cache.storeRecord(item3);

		strictEqual(cache.getRecord('item', '1'), item1);
		strictEqual(cache.getRecord('item', '2'), item2);
		strictEqual(cache.getRecord('item', '3'), item3);
	});

	test('Fetch all records of a type', function() {
		expect(3);

		var item1 = store.getRecord('item', '1');
		var item2 = store.getRecord('item', '2');
		var item3 = store.getRecord('item', '3');

		cache.storeRecord(item1);
		cache.storeRecord(item2);
		cache.storeRecord(item3);

		cache.getRecords('item').forEach(function(record) {
			ok(record === item1 || record === item2 || record === item3);
		});
	});

	test('Records expire after timeout', function() {
		expect(2);

		cache = EG.RecordCache.create({ cacheTimeout: 10 });
		var item1 = store.getRecord('item', '1');
		cache.storeRecord(item1);
		strictEqual(cache.getRecord('item', '1'), item1);

		var then = Date.now();
		while (Date.now() - then <= 10) {} // eslint-disable-line no-empty

		strictEqual(cache.getRecord('item', '1'), null);
	});

	test('Live record arrays are kept up to date', function() {
		expect(7);

		var item1 = store.getRecord('item', '1');
		var item2 = store.getRecord('item', '2');
		var item3 = store.getRecord('item', '3');

		var items = cache.getLiveRecordArray('item');
		strictEqual(items.length, 0);

		cache.storeRecord(item1);
		strictEqual(items.length, 1);
		ok(items.contains(item1));

		cache.storeRecord(item2);
		strictEqual(items.length, 2);
		ok(items.contains(item2));

		cache.storeRecord(item3);
		strictEqual(items.length, 3);
		ok(items.contains(item3));
	});
})();

<|start_filename|>tasks/options/clean.js<|end_filename|>
'use strict';

module.exports = {
	test: ['test/*.html']
};

<|start_filename|>tasks/options/yuidoc.js<|end_filename|>
module.exports = {
	compile: {
		name: 'Ember-Graph',
		options: {
			parseOnly: true,
			paths: ['./src'],
			outdir: './doc',
			tabtospace: 4
		}
	}
};

<|start_filename|>test/model/equality.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Model Equality Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: false
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: false
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: false,
						defaultValue: ['0']
					})
				}),

				tag: EG.Model.extend()
			}, {
				adapter: EG.Adapter.extend({
					deleteRecord: function(record) {
						return Em.RSVP.Promise.resolve({});
					}
				})
			});

			store.pushPayload({
				user: [
					{
						id: '1',
						posts: [
							{ type: 'post', id: '1' },
							{ type: 'post', id: '2' },
							{ type: 'post', id: '3' }
						]
					}
				],
				post: [
					{ id: '1', author: { type: 'use', id: '1' }, tags: [] },
					{ id: '1', author: { type: 'use', id: '1' }, tags: [] },
					{ id: '1', author: { type: 'use', id: '1' }, tags: [] }
				]
			});
		}
	});

	test('The same model compares correctly', function() {
		expect(3);

		var user = store.getRecord('user', '1');

		ok(user.isEqual(user));
		ok(EG.Model.isEqual(user, user));
		ok(Em.isEqual(user, user));
	});

	test('A model compares to non-model object correctly', function() {
		expect(3);

		var user = store.getRecord('user', '1');

		ok(!user.isEqual());
		ok(!user.isEqual(null));
		ok(!user.isEqual(''));
	});

	asyncTest('A proxy is equal to the real record', function() {
		expect(3);

		var realUser = store.getRecord('user', '1');
		var proxyUser = EG.PromiseObject.create({ promise: Em.RSVP.resolve(realUser) });

		proxyUser.then(function() {
			start();

			ok(realUser.isEqual(proxyUser));
			ok(EG.Model.isEqual(realUser, proxyUser));
			ok(EG.Model.isEqual(proxyUser, realUser));
		});
	});

	test('Objects of different types with the same ID aren\'t equal', function() {
		expect(2);

		var user = store.getRecord('user', '1');
		var post = store.getRecord('post', '1');

		ok(!user.isEqual(post));
		ok(!post.isEqual(user));
	});
})();

<|start_filename|>test/relationship/relationship_store.js<|end_filename|>
(function() {
	'use strict';

	var CLIENT_STATE = EG.Relationship.CLIENT_STATE;
	var SERVER_STATE = EG.Relationship.SERVER_STATE;
	var DELETED_STATE = EG.Relationship.DELETED_STATE;

	var Relationship = EG.Relationship;

	var store;

	module('Relationship Store Test', {
		setup: function() {
			store = EG.RelationshipStore.create();
		}
	});

	test('Add and fetch relationships', function() {
		expect(14);

		deepEqual(store.getCurrentRelationships('posts'), []);
		deepEqual(store.getServerRelationships('posts'), []);
		deepEqual(store.getCurrentRelationships('author'), []);
		deepEqual(store.getServerRelationships('author'), []);

		strictEqual(store.get('server.length'), 0);
		strictEqual(store.get('client.length'), 0);
		strictEqual(store.get('deleted.length'), 0);

		var r1 = Relationship.create('user', '1', 'posts', 'post', '1', 'author', SERVER_STATE);
		var r2 = Relationship.create('user', '1', 'posts', 'post', '2', 'author', SERVER_STATE);
		var r3 = Relationship.create('user', '1', 'posts', 'post', '3', 'author', DELETED_STATE);
		var r4 = Relationship.create('user', '1', 'posts', 'post', '4', 'author', CLIENT_STATE);
		var r5 = Relationship.create('user', '1', 'posts', 'post', '5', 'author', CLIENT_STATE);

		store.addRelationship('posts', r1);
		store.addRelationship('posts', r2);
		store.addRelationship('posts', r3);
		store.addRelationship('posts', r4);
		store.addRelationship('posts', r5);

		deepEqual(store.getCurrentRelationships('posts').mapBy('id').sort(), [r1, r2, r4, r5].mapBy('id').sort());
		deepEqual(store.getServerRelationships('posts').mapBy('id').sort(), [r1, r2, r3].mapBy('id').sort());
		deepEqual(store.getCurrentRelationships('author'), []);
		deepEqual(store.getServerRelationships('author'), []);

		strictEqual(store.get('server.length'), 2);
		strictEqual(store.get('client.length'), 2);
		strictEqual(store.get('deleted.length'), 1);
	});

	test('Remove relationships', function() {
		expect(13);

		var r1 = Relationship.create('user', '1', 'posts', 'post', '1', 'author', SERVER_STATE);
		var r2 = Relationship.create('user', '1', 'posts', 'post', '2', 'author', SERVER_STATE);
		var r3 = Relationship.create('user', '1', 'posts', 'post', '3', 'author', DELETED_STATE);
		var r4 = Relationship.create('user', '1', 'posts', 'post', '4', 'author', CLIENT_STATE);
		var r5 = Relationship.create('user', '1', 'posts', 'post', '5', 'author', CLIENT_STATE);

		store.addRelationship('posts', r1);
		store.addRelationship('posts', r2);
		store.addRelationship('posts', r3);
		store.addRelationship('posts', r4);
		store.addRelationship('posts', r5);

		strictEqual(store.get('server.length'), 2);
		strictEqual(store.get('client.length'), 2);
		strictEqual(store.get('deleted.length'), 1);

		store.removeRelationship(r1);
		store.removeRelationship(r4);

		strictEqual(store.get('server.length'), 1);
		strictEqual(store.get('client.length'), 1);
		strictEqual(store.get('deleted.length'), 1);

		deepEqual(store.getCurrentRelationships('posts').mapBy('id').sort(), [r2, r5].mapBy('id').sort());
		deepEqual(store.getServerRelationships('posts').mapBy('id').sort(), [r2, r3].mapBy('id').sort());

		store.clearRelationships('posts');

		deepEqual(store.getCurrentRelationships('posts'), []);
		deepEqual(store.getServerRelationships('posts'), []);

		strictEqual(store.get('server.length'), 0);
		strictEqual(store.get('client.length'), 0);
		strictEqual(store.get('deleted.length'), 0);
	});
})();

<|start_filename|>tasks/options/watch.js<|end_filename|>
module.exports = {
	options: {
		nospawn: true
	},
	code: {
		files: ['src/**/*.js'],
		tasks: ['transpile']
	},
	test: {
		files: ['test/**/*.js'],
		tasks: ['build_test_runner']
	}
};

<|start_filename|>tasks/options/groundskeeper.js<|end_filename|>
module.exports = {
	compile: {
		files: {
			'dist/ember-graph.prod.js': 'dist/ember-graph.js'
		},

		options: {
			console: false,
			'debugger': true,
			namespace: [
				'Em.assert', 'Ember.assert',
				'Em.warn', 'Ember.warn',
				'Em.runInDebug', 'Ember.runInDebug',
				'Em.deprecate', 'Ember.deprecate',
				// TODO: Clean up before transpiling
				'_ember.default.assert',
				'_ember.default.warn',
				'_ember.default.runInDebug',
				'_ember.default.deprecate'
			]
		}
	}
};

<|start_filename|>test/model/model.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Model Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					username: EG.attr({
						type: 'string',
						readOnly: true,
						defaultValue: function() {
							return 'admin';
						}
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						defaultValue: []
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						readOnly: true,
						defaultValue: null
					}),

					sharedWith: EG.hasMany({
						relatedType: 'user',
						inverse: null,
						readOnly: true,
						defaultValue: []
					})
				})
			});
		}
	});

	test('Can create a model with read-only attributes', function() {
		expect(1);

		var user = store.createRecord('user', {
			username: 'gjk'
		});

		strictEqual(user.get('username'), 'gjk');
	});

	test('Can create a model with read-only hasOne relationship', function() {
		expect(2);

		var post1 = store.createRecord('post', {
			author: '1'
		});

		deepEqual(post1.get('_author'), { type: 'user', id: '1' });

		var post2 = store.createRecord('post', {
			author: null
		});

		strictEqual(post2.get('_author'), null);
	});

	test('Can create a model with read-only hasMany relationship', function() {
		expect(1);

		var post = store.createRecord('post', {
			sharedWith: ['1', '2', '3']
		});

		deepEqual(post.get('_sharedWith').mapBy('id').sort(), ['1', '2', '3'].sort());
	});
})();

<|start_filename|>test/store/delete.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Store Delete Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: false
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: false
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: false,
						defaultValue: function() {
							return ['0'];
						}
					})
				}),

				tag: EG.Model.extend()
			}, {
				adapter: EG.Adapter.extend({
					deleteRecord: function(record) {
						return Em.RSVP.Promise.resolve({});
					}
				})
			});

			store.pushPayload({
				user: [{
					id: '1',
					posts: [
						{ type: 'post', id: '1' },
						{ type: 'post', id: '2' },
						{ type: 'post', id: '3' }
					]
				}],
				post: [
					{ id: '1', author: { type: 'user', id: '1' }, tags: [] },
					{ id: '2', author: { type: 'user', id: '1' }, tags: [] },
					{ id: '3', author: { type: 'user', id: '1' }, tags: [] }
				]
			});
		}
	});

	asyncTest('Relationships are properly disconnected on delete', function() {
		expect(6);

		var post1 = store.getRecord('post', '1');
		var post2 = store.getRecord('post', '2');
		var post3 = store.getRecord('post', '3');

		start();
		deepEqual(post1.get('_author'), { type: 'user', id: '1' });
		deepEqual(post2.get('_author'), { type: 'user', id: '1' });
		deepEqual(post3.get('_author'), { type: 'user', id: '1' });
		stop();

		store.getRecord('user', '1').destroy().then(function() {
			start();

			strictEqual(post1.get('_author'), null);
			strictEqual(post2.get('_author'), null);
			strictEqual(post3.get('_author'), null);
		});
	});

	asyncTest('Delete new record', function() {
		expect(3);

		var user = store.getRecord('user', '1');
		var post = store.createRecord('post', {
			author: '1'
		});
		var id = post.get('id');

		start();
		ok(user.get('_posts').toArray().mapBy('id').indexOf(id) >= 0);
		stop();

		post.destroy().then(function() {
			start();

			strictEqual(post.get('store'), null);
			ok(user.get('_posts').toArray().indexOf(id) < 0);
		});
	});

	test('Delete records with `deletedRecords` meta attribute', function() {
		expect(2);

		var user = store.getRecord('user', '1');
		deepEqual(user.get('_posts').mapBy('id').sort(), ['1', '2', '3'].sort());

		store.pushPayload({
			meta: {
				serverMeta: {
					deletedRecords: [
						{ type: 'post', id: '1' },
						{ type: 'post', id: '2' },
						{ type: 'post', id: '3' }
					]
				}
			}
		});

		deepEqual(user.get('_posts'), []);
	});
})();

<|start_filename|>tasks/options/sass.js<|end_filename|>
module.exports = {
	dist: {
		files: {
			'site_build/stylesheets/style.css': 'site/stylesheets/style.scss'
		}
	}
};

<|start_filename|>src/adapter/rest.js<|end_filename|>
import $ from 'jquery';
import Ember from 'ember';
import copy from 'ember-graph/util/copy';
import Adapter from 'ember-graph/adapter/adapter';

import { pluralize } from 'ember-graph/util/inflector';
import { computed } from 'ember-graph/util/computed';

const Promise = Ember.RSVP.Promise;

/**
 * An adapter that communicates with REST back-ends. The requests made all follow the
 * {{link-to 'JSON API' 'http://jsonapi.org/format/'}} standard. Because the standard
 * is constantly evolving, you should check the documentation for the individual
 * methods to ensure that they're doing what you expect.
 *
 * @class RESTAdapter
 * @extends Adapter
 * @constructor
 */
export default Adapter.extend({

	urlLengthLimit: 2048,

	/**
	 * Sends a `POST` request to `/{pluralized_type}` with the serialized record as the body.
	 *
	 * @method createRecord
	 * @param {Model} record
	 * @return {Promise} A promise that resolves to the created record
	 */
	createRecord: function(record) {
		var _this = this;
		var typeKey = record.get('typeKey');
		var url = this.buildUrl(typeKey);
		var json = this.serialize(record, { requestType: 'createRecord', recordType: typeKey });

		return this.ajax(url, 'POST', json).then(function(payload) {
			return _this.deserialize(payload, { requestType: 'createRecord', recordType: typeKey });
		});
	},

	/**
	 * Sends a `GET` request to `/{pluralized_type}/{id}`.
	 *
	 * @method findRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {Promise} A promise that resolves to the requested record
	 */
	findRecord: function(typeKey, id) {
		var _this = this;
		var url = this.buildUrl(typeKey, id);

		return this.ajax(url, 'GET').then(function(payload) {
			return _this.deserialize(payload, { requestType: 'findRecord', recordType: typeKey, id: id });
		});
	},

	/**
	 * Sends a `GET` request to `/{pluralized_type}/{id},{id},{id}`
	 *
	 * @method findMany
	 * @param {String} typeKey
	 * @param {String[]} ids
	 * @return {Promise} A promise that resolves to an array of requested records
	 */
	findMany: function(typeKey, ids) {
		const url = this.buildUrl(typeKey, ids.join(','));

		if (window.location.origin.length + url.length <= this.get('urlLengthLimit')) {
			return this.ajax(url, 'GET').then((payload) => {
				return this.deserialize(payload, { requestType: 'findMany', recordType: typeKey, ids: ids });
			});
		}

		const urls = this.buildMultipleUrls(typeKey, ids);
		const promises = urls.map((url) => this.ajax(url, 'GET'));

		return Promise.all(promises).then((payloads) => {
			const payload = this.mergePayloads(payloads);
			return this.deserialize(payload, { requestType: 'findMany', recordType: typeKey, ids });
		});
	},

	/**
	 * Sends a `GET` request to `/{pluralized_type}`.
	 *
	 * @method findAll
	 * @param {String} typeKey
	 * @return {Promise} A promise that resolves to an array of requested records
	 */
	findAll: function(typeKey) {
		var _this = this;
		var url = this.buildUrl(typeKey);

		return this.ajax(url, 'GET').then(function(payload) {
			return _this.deserialize(payload, { requestType: 'findAll', recordType: typeKey });
		});
	},

	/**
	 * Sends a `GET` request to `/{pluralized_type}?option=value`.
	 *
	 * @method findQuery
	 * @param {String} typeKey
	 * @param {Object} query An object with query parameters to serialize into the URL
	 * @return {Promise} A promise that resolves to an array of requested records
	 */
	findQuery: function(typeKey, query) {
		var _this = this;
		var options = {};

		Object.keys(query).forEach(function(key) {
			options[key] = '' + query[key];
		});

		var url = this.buildUrl(typeKey, null, options);

		return this.ajax(url, 'GET').then(function(payload) {
			return _this.deserialize(payload, { requestType: 'findQuery', recordType: typeKey, query: query });
		});
	},

	/**
	 * Sends a `PATCH` request to `/{pluralized_type}/{id}` with the record's
	 * changes serialized to JSON change operations. The change operations
	 * use the path format described by the standard. See the example below:
	 *
	 * ```json
	 * [
	 *     { op: "replace", path: "/title", value: "Getting Started With Ember-Graph" },
	 *     { op: "replace", path: "/links/author", value: "24" },
	 *     { op: "add", path: "/links/tags/-", value: "73" },
	 *     { op: "remove", path: "/links/109" }
	 * ]
	 * ```
	 *
	 * @method updateRecord
	 * @param {Model} record
	 * @return {Promise} A promise that resolves to the updated record
	 */
	updateRecord: function(record) {
		var _this = this;
		var url = this.buildUrl(record.typeKey, record.get('id'));
		var json = this.serialize(record, { requestType: 'updateRecord' });

		if (json.length <= 0) {
			return Promise.resolve();
		}

		return this.ajax(url, 'PATCH', json).then(function(payload) {
			return _this.deserialize(payload, { requestType: 'updateRecord', recordType: record.typeKey });
		});
	},

	/**
	 * Sends a `DELETE` request to `/{pluralized_type}/{id}`.
	 *
	 * @method deleteRecord
	 * @param {Model} record
	 * @return {Promise} A promise that resolves on success and rejects on failure
	 */
	deleteRecord: function(record) {
		var _this = this;
		var url = this.buildUrl(record.typeKey, record.get('id'));

		return this.ajax(url, 'DELETE').then(function(payload) {
			var options = { requestType: 'deleteRecord', recordType: record.typeKey };
			return _this.deserialize(payload, options);
		});
	},

	/**
	 * This function will build the URL that the request will be posted to.
	 * The options must be strings, but they don't have to be escaped,
	 * this function will do that.
	 *
	 * @method buildUrl
	 * @param {String} typeKey
	 * @param {String} [id]
	 * @param {Object} [options]
	 * @return {String}
	 * @protected
	 */
	buildUrl: function(typeKey, id, options) {
		var url = this.get('prefix') + '/' + pluralize(typeKey);

		if (id) {
			url += '/' + id;
		}

		if (options) {
			Object.keys(options).forEach(function(key, index) {
				url += ((index === 0) ? '?' : '&') + key + '=' + encodeURIComponent(options[key]);
			});
		}

		return url;
	},

	buildMultipleUrls(typeKey, ids) {
		const base = this.get('prefix') + '/' + pluralize(typeKey) + '/';
		const baseLength = (window.location.origin.length + base.length);
		const lengthLimit = this.get('urlLengthLimit');

		const idArrays = ids.reduce((idArrays, id) => {
			const idArray = idArrays[idArrays.length - 1];

			if (baseLength + idArray.join(',').length + id.length + 1 <= lengthLimit) {
				idArray.push(id);
			} else {
				idArrays.push([id]);
			}

			return idArrays;
		}, [[]]);

		return idArrays.map((idArray) => base + idArray.join(','));
	},

	mergePayloads(payloads) {
		const mergeObjects = (a, b) => {
			const merged = Ember.assign(copy(a, true), b);

			for (let key in b) {
				if (b.hasOwnProperty(key) && a.hasOwnProperty(key)) {
					if (Ember.isArray(b[key])) {
						merged[key] = a[key].concat(b[key]);
					} else if (b[key] && typeof b[key] === 'object') {
						merged[key] = mergeObjects(a[key], b[key]);
					}
				}
			}

			return merged;
		};

		return payloads.reduce(mergeObjects, {});
	},

	/**
	 * This property is used by the adapter when forming the URL for requests.
	 * The adapter normally makes requests to the current location. So the URL
	 * looks like `/users/6`. If you want to add a different host, or a prefix,
	 * override this property.
	 *
	 * Warning: Do **not** include a trailing slash. The adapter won't check for
	 * mistakes, so just don't do it.
	 *
	 * @property prefix
	 * @type String
	 * @default ''
	 */
	prefix: computed({
		get() {
			return '';
		}
	}),

	/**
	 * This method sends the request to the server.
	 * The response is processed in the Ember run-loop.
	 *
	 * @method ajax
	 * @param {String} url
	 * @param {String} verb `GET`, `POST`, `PATCH` or `DELETE`
	 * @param {String} [body]
	 * @return {Promise}
	 * @protected
	 */
	ajax: function(url, verb, body) {
		var headers = this.headers(url, verb, body);

		return new Promise(function(resolve, reject) {
			$.ajax({
				cache: false,
				contentType: 'application/json',
				data: body && (Ember.typeOf(body) === 'string' ? body : JSON.stringify(body)),
				headers: headers,
				processData: false,
				type: verb,
				url: url,

				error: function(jqXHR, textStatus, error) {
					Ember.run(null, reject, error);
				},

				success: function(data, status, jqXHR) {
					Ember.run(null, resolve, data);
				}
			});
		});
	},

	/**
	 * This is a small hook to allow including extra headers in the AJAX request.
	 *
	 * @method headers
	 * @param {String} url
	 * @param {String} verb `GET`, `POST`, `PATCH` or `DELETE`
	 * @param {String} [body]
	 * @return {Object} Headers to give to jQuery `ajax` function
	 * @protected
	 */
	headers: function(url, verb, body) {
		return {};
	}
});


<|start_filename|>test/attribute_type/array.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var type = EG.ArrayType.create();

	module('Array Attribute Type Test', {
		setup: function() {
			store = setupStore({
				lotteryTicket: EG.Model.extend({
					numbers: EG.attr('array')
				})
			});

			store.pushPayload({
				lotteryTicket: [
					{
						id: '1',
						numbers: [4, 8, 15, 16, 23, 42]
					}
				]
			});
		}
	});

	test('Comparison works correctly', function() {
		expect(5);

		ok(type.isEqual([], []));
		ok(type.isEqual(['hello'], ['hello']));
		ok(type.isEqual([1, 2, 3], [1, 2, 3]));
		ok(!type.isEqual([1, 2], [2, 1]));
		ok(!type.isEqual([''], [null]));
	});

	test('Changes can be observed', 2, function() {
		var ticket = store.getRecord('lotteryTicket', '1');

		ticket.addObserver('numbers.[]', function() {
			ok(true);
		});

		ticket.get('numbers').pushObject(0);
		ticket.set('numbers', ticket.get('numbers').slice(0, 6));
	});
})();

<|start_filename|>test/util/inflector.js<|end_filename|>
(function() {
	'use strict';

	module('Inflector Test');

	test('Can override singular rules', 2, function() {
		strictEqual(EmberGraph.Inflector.pluralize('word'), 'words');
		EmberGraph.Inflector.overridePluralRule('word', 'foobar');
		strictEqual(EmberGraph.Inflector.pluralize('word'), 'foobar');
	});

	test('Can override plural rules', 2, function() {
		strictEqual(EmberGraph.Inflector.singularize('words'), 'word');
		EmberGraph.Inflector.overrideSingularRule('word', 'foobar');
		strictEqual(EmberGraph.Inflector.singularize('word'), 'foobar');
	});
})();


<|start_filename|>src/model/model.js<|end_filename|>
import Ember from 'ember';
import CoreModel from 'ember-graph/model/core';
import StateProperties from 'ember-graph/model/states';
import RelationshipLoadMethods from 'ember-graph/model/relationship_load';

import { generateUUID } from 'ember-graph/util/util';
import { startsWith } from 'ember-graph/util/string';
import { computed } from 'ember-graph/util/computed';
import {
	RelationshipClassMethods,
	RelationshipPublicMethods,
	RelationshipPrivateMethods,
	HAS_ONE_KEY,
	HAS_MANY_KEY
} from 'ember-graph/model/relationship';


/**
 * Models are the classes that represent your domain data.
 * Each type of object in your domain should have its own
 * model, with attributes and relationships declared using the
 * [attr](EG.html#method_attr), [hasOne](EG.html#method_hasOne)
 * and [hasMany](EG.html#method_hasMany) functions.
 *
 * To create a model, subclass this class (or any other Model
 * subclass) and place it your app's namespace. The name
 * that you give it is important, since that's how it will be
 * looked up by the container. The usual convention is to use
 * a camel-cased name like `App.PostComment` or `App.ForumAdmin`.
 * For more information on resolving, read the Ember.js entry
 * on the [DefaultResolver](http://emberjs.com/api/classes/Ember.DefaultResolver.html).
 *
 * @class Model
 * @extends CoreModel
 * @uses Ember.Evented
 */
var Model = CoreModel.extend(Ember.Evented, {

	/**
	 * This property is available on every model instance and every
	 * model subclass (after being looked up at least once by the
	 * container). This is the key that you use to refer to the model
	 * in relationships and store methods. Examples:
	 *
	 * ```
	 * App.User => user
	 * App.PostComment => postComment
	 * ```
	 *
	 * @property typeKey
	 * @type String
	 * @final
	 */
	typeKey: null,

	_id: null,

	/**
	 * The ID of the record. The ID can only be changed once, and only if
	 * it's being changed from a temporary ID to a permanent one. Only the
	 * store should change the ID from a temporary one to a permanent one.
	 *
	 * @property id
	 * @type String
	 * @final
	 */
	id: computed('_id', {
		get() {
			return this.get('_id');
		},
		set(key, value) {
			const id = this.get('_id');
			const prefix = this.constructor.temporaryIdPrefix;

			if (id === null || (startsWith(id, prefix) && !startsWith(value, prefix))) {
				this.set('_id', value);
			} else {
				throw new Ember.Error('Cannot change the \'id\' property of a model.');
			}
		}
	}),

	/**
	 * @property store
	 * @type Store
	 * @final
	 */
	store: null,

	/**
	 * Loads JSON data from the server into the record. This may be used when
	 * the record is brand new, or when the record is being reloaded. This
	 * should generally only be used by the store or for testing purposes.
	 * However, this can be useful to override to intercept data before it's
	 * loaded into the record;
	 *
	 * @method loadData
	 * @param {Object} json
	 * @deprecated Use `loadDataFromServer` instead
	 */
	loadData() {
		return this.loadDataFromServer(...arguments);
	},

	/**
	 * Takes a payload from the server and merges the data into the current data.
	 * This is generally only called by the store, but it may be useful to
	 * override it if you're looking to intercept and modify server data before
	 * it's loaded into the record.
	 *
	 * @method loadDataFromServer
	 * @param {Object} json
	 */
	loadDataFromServer(json = {}) {
		Ember.assert('The record `' + this.typeKey + ':' + this.get('id') + '` was attempted to be reloaded ' +
			'while dirty with `reloadDirty` disabled.', !this.get('isDirty') || this.get('store.reloadDirty'));

		this.loadAttributesFromServer(json);
		this.loadRelationshipsFromServer(json);
	},

	/**
	 * Takes the data passed to the store's {{link-to-method 'Store' 'createRecord'}}
	 * method and loads it into the newly created record by calling the model's
	 * public API methods for manipulating records. This should really only be
	 * called by the store and when a record is brand new.
	 *
	 * @method initializeRecord
	 * @param {Object} json
	 */
	initializeRecord: function(json = {}) {
		this.initializeAttributes(json);
		this.initializeRelationships(json);
	},

	/**
	 * Proxies the store's save method for convenience.
	 *
	 * @method save
	 * @return Promise
	 */
	save: function() {
		var _this = this;
		var property = null;

		if (this.get('isNew')) {
			property = 'isCreating';
		} else {
			property = 'isSaving';
		}

		this.set(property, true);
		return this.get('store').saveRecord(this).finally(function() {
			_this.set(property, false);
		});
	},

	/**
	 * Proxies the store's reload method for convenience.
	 *
	 * @method reload
	 * @return Promise
	 */
	reload: function() {
		var _this = this;

		this.set('isReloading', true);
		return this.get('store').reloadRecord(this).finally(function() {
			_this.set('isReloading', false);
		});
	},

	/**
	 * Proxies the store's delete method for convenience.
	 *
	 * @method destroyRecord
	 * @return Promise
	 */
	destroyRecord: function() {
		var _this = this;

		this.set('isDeleting', true);
		return this.get('store').deleteRecord(this).then(function() {
			_this.set('isDeleted', true);
			_this.destroy();
		}).finally(function() {
			_this.set('isDeleting', false);
		});
	},

	/**
	 * Determines if the other object is a model that represents the same record.
	 *
	 * @method isEqual
	 * @return Boolean
	 */
	isEqual: function(other) {
		if (!other) {
			return;
		}

		return (this.typeKey === Ember.get(other, 'typeKey') && this.get('id') === Ember.get(other, 'id'));
	},

	/**
	 * Determines if the newly created record is fully initialized yet.
	 * If it's not initialized, it can't be persisted to the server.
	 * This will always return `true` for non-new records.
	 */
	isInitialized: function() {
		return !this.get('isNew') || (this.areAttributesInitialized() && this.areRelationshipsInitialized());
	},

	/**
	 * Rolls back changes to both attributes and relationships.
	 *
	 * @method rollback
	 */
	rollback: function() {
		this.rollbackAttributes();
		this.rollbackRelationships();
	}
});

Model.reopenClass({

	/**
	 * The prefix added to generated IDs to show that the prefix wasn't given
	 * by the server and is only temporary until the real one comes in.
	 *
	 * @property temporaryIdPrefix
	 * @type String
	 * @static
	 */
	temporaryIdPrefix: 'EG_TEMP_ID_',

	/**
	 * @method isTemporaryId
	 * @param {String} id
	 * @return Boolean
	 * @static
	 */
	isTemporaryId: function(id) {
		return startsWith(id, this.temporaryIdPrefix);
	},

	/**
	 * This method creates a record shell, initializing the `store` and `id` properties.
	 * (The ID is a temporary ID.) **This can only be called by the store.** Calling it
	 * yourself will decouple the record from the store, causing odd behavior.
	 *
	 * @method create
	 * @param {Store} store
	 * @return {Model}
	 */
	create: function(store) {
		var record = this._super();
		record.set('store', store);
		record.set('_id', Ember.get(this, 'temporaryIdPrefix') + generateUUID());
		return record;
	},

	/**
	 * @method extend
	 * @static
	 */
	extend: function() {
		var args = Array.prototype.slice.call(arguments, 0);
		var options = args.pop() || {};
		var attributes = {};
		var relationships = {};

		// Ember.Mixin doesn't have a `detectInstance` method
		if (!(options instanceof Ember.Mixin)) {
			Object.keys(options).forEach(function(key) {
				var value = options[key];

				if (options[key]) {
					if (options[key].isRelationship) {
						relationships[key] = value;
						delete options[key];
					} else if (options[key].isAttribute) {
						attributes[key] = value;
						delete options[key];
					}
				}
			});
		}

		args.push(options);

		var subclass = this._super.apply(this, args);
		subclass.declareAttributes(attributes);
		subclass.declareRelationships(relationships);
		return subclass;
	},

	/**
	 * Determines if the two objects passed in are equal models (or model proxies).
	 *
	 * @param {Model} a
	 * @param {Model} b
	 * @return Boolean
	 * @static
	 */
	isEqual: function(a, b) {
		if (Ember.isNone(a) || Ember.isNone(b)) {
			return false;
		}

		if (this.detectInstance(a)) {
			return a.isEqual(b);
		}

		if (this.detectInstance(b)) {
			return b.isEqual(a);
		}

		if (this.detectInstance(Ember.get(a, 'content'))) {
			return Ember.get(a, 'content').isEqual(b);
		}

		if (this.detectInstance(Ember.get(b, 'content'))) {
			return Ember.get(b, 'content').isEqual(a);
		}

		return false;
	}
});

Model.reopen(StateProperties);
Model.reopen(RelationshipPublicMethods);
Model.reopen(RelationshipPrivateMethods);
Model.reopen(RelationshipLoadMethods);
Model.reopenClass(RelationshipClassMethods);
Model.reopenClass({
	HAS_ONE_KEY: HAS_ONE_KEY,
	HAS_MANY_KEY: HAS_MANY_KEY
});

export default Model;


<|start_filename|>test/attribute_type/enum.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.EnumType.extend({
		defaultValue: 'ORANGE',
		values: ['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'INDIGO', 'VIOLET']
	}).create();

	module('Enum Attribute Type Test');

	test('Values are serialized correctly', function() {
		expect(7);

		strictEqual(type.serialize('BLUE'), 'BLUE');
		strictEqual(type.serialize('RED'), 'RED');
		strictEqual(type.serialize('green'), 'green');
		strictEqual(type.serialize(''), 'ORANGE');
		strictEqual(type.serialize(null), 'ORANGE');
		strictEqual(type.serialize(new String('INDIGO')), 'INDIGO'); // eslint-disable-line no-new-wrappers
		strictEqual(type.serialize(String('violet')), 'violet');
	});

	test('Values are compared correctly', function() {
		expect(7);

		ok(type.isEqual('RED', 'RED'));
		ok(type.isEqual('orange', 'orange'));
		ok(type.isEqual('yellow', 'YELLOW'));
		ok(type.isEqual(String('Green'), 'green'));
		ok(!type.isEqual('BLUE', 'RED'));
		ok(!type.isEqual(null, 'RED'));
		ok(!type.isEqual());
	});
})();

<|start_filename|>test/model/id.js<|end_filename|>
(function() {
	'use strict';

	var store = setupStore({ test: EG.Model.extend() });

	module('Model ID Test');

	test('Existing ID loads correctly', function() {
		expect(1);

		store.pushPayload({
			test: [{ id: 'TEST_ID' }]
		});

		strictEqual(store.getRecord('test', 'TEST_ID').get('id'), 'TEST_ID');
	});

	test('New ID is created', function() {
		expect(1);

		var model = store.createRecord('test', {});

		ok(EG.String.startsWith(model.get('id'), EG.Model.temporaryIdPrefix));
	});

	test('A permanent ID cannot be changed', function() {
		expect(1);

		store.pushPayload({
			test: [{ id: '1' }]
		});

		var model = store.getRecord('test', '1');

		throws(function() {
			model.set('id', '');
		});
	});

	test('A temporary ID can be changed to a permanent one', function() {
		expect(1);

		var model = store.createRecord('test', {});
		model.set('id', '');

		ok(model.get('id') === '');
	});
})();



<|start_filename|>src/util/set.js<|end_filename|>
import Ember from 'ember';
import Copyable from 'ember-graph/util/copyable';

/* eslint-disable */
/**
 * Pulled from the Ember 1.13 release.
 *
 * withoutAll added by me
 *
 * TODO: Remove and use ES6 Set
 */
export default Ember.CoreObject.extend(Ember.MutableArray, Copyable, {

	length: 0,

	clear() {
		var len = Ember.get(this, 'length');
		if (len === 0) { return this; }

		var guid;

		this.arrayContentWillChange(len, 0);

		for (var i=0; i < len; i++) {
			guid = Ember.guidFor(this[i]);
			delete this[guid];
			delete this[i];
		}

		Ember.set(this, 'length', 0);

		Ember.notifyPropertyChange(this, 'firstObject');
		Ember.notifyPropertyChange(this, 'lastObject');
		this.arrayContentDidChange(len, 0);

		return this;
	},

	isEqual(obj) {
		// fail fast
		if (!Ember.Enumerable.detect(obj)) {
			return false;
		}

		var loc = Ember.get(this, 'length');
		if (Ember.get(obj, 'length') !== loc) {
			return false;
		}

		while (--loc >= 0) {
			if (!obj.includes(this[loc])) {
				return false;
			}
		}

		return true;
	},

	add() {
		return this.addObject(...arguments);
	},

	remove() {
		return this.removeObject(...arguments);
	},

	pop() {
		var obj = this.length > 0 ? this[this.length-1] : null;
		this.remove(obj);
		return obj;
	},

	push() {
		return this.addObject(...arguments);
	},

	shift() {
		return this.pop(...arguments);
	},

	unshift() {
		return this.push(...arguments);
	},

	addEach() {
		return this.addObject(...arguments);
	},

	removeEach() {
		return this.removeObjects(...arguments);
	},

	init(items) {
		this._super(...arguments);

		if (items) {
			this.addObjects(items);
		}
	},

	nextObject(idx) {
		return this[idx];
	},

	firstObject: Ember.computed(function() {
		return this.length > 0 ? this[0] : undefined;
	}),

	lastObject: Ember.computed(function() {
		return this.length > 0 ? this[this.length-1] : undefined;
	}),

	addObject(obj) {
		if (Ember.isNone(obj)) {
			return this; // nothing to do
		}

		var guid = Ember.guidFor(obj);
		var idx  = this[guid];
		var len  = Ember.get(this, 'length');
		var added;

		if (idx>=0 && idx<len && (this[idx] === obj)) {
			return this; // added
		}

		added = [obj];

		this.arrayContentWillChange(null, added);

		len = Ember.get(this, 'length');
		this[guid] = len;
		this[len] = obj;
		Ember.set(this, 'length', len+1);

		Ember.notifyPropertyChange(this, 'lastObject');
		this.arrayContentDidChange(null, added);

		return this;
	},

	removeObject(obj) {
		if (Ember.isNone(obj)) {
			return this; // nothing to do
		}

		var guid = Ember.guidFor(obj);
		var idx  = this[guid];
		var len = Ember.get(this, 'length');
		var isFirst = idx === 0;
		var isLast = idx === len-1;
		var last, removed;


		if (idx>=0 && idx<len && (this[idx] === obj)) {
			removed = [obj];

			this.arrayContentWillChange(removed, null);

			// swap items - basically move the item to the end so it can be removed
			if (idx < len-1) {
				last = this[len-1];
				this[idx] = last;
				this[Ember.guidFor(last)] = idx;
			}

			delete this[guid];
			delete this[len-1];
			Ember.set(this, 'length', len-1);

			if (isFirst) { Ember.notifyPropertyChange(this, 'firstObject'); }
			if (isLast) { Ember.notifyPropertyChange(this, 'lastObject'); }
			this.arrayContentDidChange(removed, null);
		}

		return this;
	},

	contains(obj) {
		return this[Ember.guidFor(obj)]>=0;
	},

	copy() {
		var C = this.constructor;
		var ret = new C();
		var loc = Ember.get(this, 'length');

		set(ret, 'length', loc);
		while (--loc >= 0) {
			ret[loc] = this[loc];
			ret[Ember.guidFor(this[loc])] = loc;
		}
		return ret;
	},

	toString() {
		var len = this.length;
		var array = [];
		var idx;

		for (idx = 0; idx < len; idx++) {
			array[idx] = this[idx];
		}
		return 'Ember.Set<' + array.join(',') + '>';
	},

	withoutAll(items) {
		var ret = this.copy();
		ret.removeObjects(items);
		return ret;
	}
});
/* eslint-enable */


<|start_filename|>test/attribute_type/object.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.ObjectType.create();

	module('Object Attribute Type Test');

	test('Empty objects are equal', function() {
		expect(1);

		ok(type.isEqual({}, {}));
	});

	test('Object constructors work too', function() {
		expect(1);

		ok(type.isEqual(Object(), Object()));
	});

	test('Simple objects are equal', function() {
		expect(1);

		var a = { foo: 1, bar: 2 };
		var b = { bar: 2, foo: 1 };

		ok(type.isEqual(a, b));
	});

	test('Type coercions aren\'t performed', function() {
		expect(1);

		var a = { foo: '1' };
		var b = { foo: 1 };

		ok(!type.isEqual(a, b));
	});

	test('Arrays compare equally', function() {
		expect(1);

		var a = { arr: [1, 2, 3, 4, 5] };
		var b = { arr: [1, 2, 3, 4, 5] };

		ok(type.isEqual(a, b));
	});

	test('Nested objects compare equally', function() {
		expect(1);

		var a = {
			a: null,
			o: {
				2: 2,
				3: 4,
				1: 1
			}
		};
		var b = {
			a: null,
			o: {
				1: 1,
				2: 2,
				3: 4
			}
		};

		ok(type.isEqual(a, b));
	});

	test('Non-objects always fail', function() {
		expect(8);

		ok(!type.isEqual());
		ok(!type.isEqual(null, null));
		ok(!type.isEqual(true, true));
		ok(!type.isEqual(0, 0));
		ok(!type.isEqual('', ''));
		ok(!type.isEqual(NaN, NaN));
		ok(!type.isEqual([], []));
		ok(!type.isEqual(new Date(), new Date()));
	});
})();

<|start_filename|>test/serializer/json_polymorphic.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var serializer;

	var jsonSort = function(a, b) {
		// Order doesn't matter, as long as it's consistent
		return (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1);
	};

	module('JSON Serializer Test (Polymorphic)', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					email: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: true
					})
				}),

				post: EG.Model.extend({
					title: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					body: EG.attr({
						type: 'string',
						isRequired: false,
						defaultValue: ''
					}),

					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: true
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: true
					})
				}),

				tag: EG.Model.extend({
					name: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					})
				})
			});

			serializer = EG.JSONSerializer.create({ store: store, polymorphicRelationships: true });

			store.pushPayload({
				user: [{
					id: '1',
					email: '<EMAIL>',
					posts: [{ type: 'post', id: '1' }, { type: 'post', id: '2' }]
				}],
				post: [{
					id: '1',
					title: 'Title Post 1',
					body: 'Body1',
					author: { type: 'user', id: '1' },
					tags: [{ type: 'tag', id: '1' }, { type: 'tag', id: '2' }]
				}, {
					id: '2',
					title: 'Title Post 2',
					body: 'Body2',
					author: { type: 'user', id: '1' },
					tags: [{ type: 'tag', id: '3' }, { type: 'tag', id: '4' }]
				}],
				tag: [
					{ id: '1', name: 'foo' },
					{ id: '2', name: 'bar' },
					{ id: '3', name: 'spam' },
					{ id: '4', name: 'eggs' }
				]
			});
		}
	});

	test('Deserialize a normal payload', function() {
		expect(1);

		var payload = {
			posts: [
				{
					id: 3,
					title: 'Test Post 3',
					links: {
						author: { type: 'user', id: 1 },
						tags: [{ type: 'tag', id: 1 }, { type: 'tag', id: 3 }]
					}
				},
				{
					id: 4,
					title: 'Test Post 4',
					body: 'Body4',
					links: {
						author: { type: 'user', id: '1' },
						tags: [{ type: 'tag', id: '2' }, { type: 'tag', id: '4' }]
					}
				}
			],
			linked: {
				tags: [
					{ id: 10, name: '10' },
					{ id: 11, name: '11' }
				]
			}
		};

		var expected = {
			meta: {
				serverMeta: {}
			},
			post: [
				{
					id: '3',
					title: 'Test Post 3',
					body: '',
					author: { type: 'user', id: '1' },
					tags: [{ type: 'tag', id: '1' }, { type: 'tag', id: '3' }]
				},
				{
					id: '4',
					title: 'Test Post 4',
					body: 'Body4',
					author: { type: 'user', id: '1' },
					tags: [{ type: 'tag', id: '2' }, { type: 'tag', id: '4' }]
				}
			],
			tag: [
				{ id: '10', name: '10' },
				{ id: '11', name: '11' }
			]
		};

		var normalized = serializer.deserialize(payload, { requestType: 'findAll', recordType: 'post' });

		deepEqual(normalized, expected);
	});

	test('Deserialize a create record payload', function() {
		expect(1);

		var payload = {
			posts: [{
				id: 200,
				title: '',
				links: {
					author: { type: 'user', id: 1 },
					tags: []
				}
			}]
		};

		var expected = {
			meta: {
				serverMeta: {},
				createdRecord: { type: 'post', id: '200' }
			},
			post: [{
				id: '200',
				title: '',
				body: '',
				author: { type: 'user', id: '1' },
				tags: []
			}]
		};

		var normalized = serializer.deserialize(payload, { requestType: 'createRecord', recordType: 'post' });

		deepEqual(normalized, expected);
	});

	test('Deserialize a query payload', function() {
		expect(1);

		var query = { foo: 'bar', spam: 1, eggs: [] };

		var payload = {
			posts: [
				{ id: 158, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } },
				{ id: 98, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } },
				{ id: 262, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } },
				{ id: 0, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } }
			]
		};

		var expected = {
			meta: {
				serverMeta: {},
				matchedRecords: [
					{ type: 'post', id: '158' },
					{ type: 'post', id: '98' },
					{ type: 'post', id: '262' },
					{ type: 'post', id: '0' }
				]
			},

			post: [
				{ id: '158', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] },
				{ id: '98', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] },
				{ id: '262', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] },
				{ id: '0', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] }
			]
		};

		var serializerOptions = { requestType: 'findQuery', recordType: 'post', query: query };
		var normalized = serializer.deserialize(payload, serializerOptions);

		deepEqual(normalized, expected);
	});

	test('Invalid/missing values during deserialization throw errors', function() {
		expect(4);

		var options = { requestType: 'findRecord', recordType: 'post' };

		throws(function() {
			serializer.deserialize({
				posts: [{
					title: '',
					links: {
						author: { type: 'user', id: 1 },
						tags: []
					}
				}]
			}, options);
		}, /invalid id/i,  'Missing ID');

		throws(function() {
			serializer.deserialize({
				posts: [{
					id: {},
					title: '',
					links: {
						author: { type: 'user', id: 1 },
						tags: []
					}
				}]
			}, options);
		}, /invalid id/i, 'Invalid ID');

		throws(function() {
			serializer.deserialize({
				posts: [{
					id: 0,
					links: {
						author: { type: 'user', id: 1 },
						tags: []
					}
				}]
			}, options);
		}, /attribute was missing/i, 'Missing attribute');

		throws(function() {
			serializer.deserialize({
				posts: [{
					id: 0,
					title: '',
					links: {
						author: { type: 'user', id: 1 }
					}
				}]
			}, options);
		}, /missing `.*` relationship/i, 'Missing relationship');

// TODO: PhantomJS bug?
//		throws(function() {
//			serializer.deserialize({
//				posts: [{
//					id: 0,
//					title: '',
//					links: {
//						author: { type: 'user', id: 1 },
//						tags: null
//					}
//				}]
//			}, options);
//		});
	});

	test('Serialize a create record request', function() {
		expect(1);

		var post = store.createRecord('post', {
			title: 'title',
			body: 'body',
			author: '1',
			tags: []
		});

		var expected = {
			posts: [{
				title: 'title',
				body: 'body',
				links: {
					author: { type: 'user', id: '1' },
					tags: []
				}
			}]
		};

		var serialized = serializer.serialize(post, { requestType: 'createRecord' });

		deepEqual(serialized, expected);
	});

	test('Serialize a create record request (2)', function() {
		expect(1);

		var post = store.createRecord('post', {
			title: 'title',
			body: 'body',
			author: null,
			tags: []
		});

		var expected = {
			posts: [{
				title: 'title',
				body: 'body',
				links: {
					author: null,
					tags: []
				}
			}]
		};

		var serialized = serializer.serialize(post, { requestType: 'createRecord' });

		deepEqual(serialized, expected);
	});

	test('Serialize an update request properly', function() {
		expect(1);

		var post = store.getRecord('post', '1');

		post.removeFromRelationship('tags', '2');
		post.addToRelationship('tags', '7');
		post.clearHasOneRelationship('author');
		post.set('body', null);

		var expected = [
			{ op: 'remove', path: '/links/tags/2', value: { type: 'tag', id: '2' } },
			{ op: 'add', path: '/links/tags', value: { type: 'tag', id: '7' } },
			{ op: 'replace', path: '/links/author', value: null },
			{ op: 'replace', path: '/body', value: null }
		];

		var operations = serializer.serialize(post, { requestType: 'updateRecord' });

		deepEqual(operations.sort(jsonSort), expected.sort(jsonSort));
	});

	test('Deserialize payload with no options', function() {
		expect(1);

		var payload = {
			posts: [
				{ id: 158, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } },
				{ id: 98, title: '', links: { author: { type: 'user', id: 1 }, tags: [{ type: 'tag', id: 10 }] } },
				{ id: 26, title: '', links: { author: { type: 'user', id: 1 }, tags: [{ type: 'tag', id: 11 }] } },
				{ id: 0, title: '', links: { author: { type: 'user', id: 1 }, tags: [] } }
			],
			tags: [
				{ id: 10, name: '10' },
				{ id: 11, name: '11' }
			]
		};

		var expected = {
			meta: {
				serverMeta: {}
			},

			post: [
				{ id: '158', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] },
				{ id: '98', title: '', body: '', author: { type: 'user', id: '1' }, tags: [{ type: 'tag', id: '10' }] },
				{ id: '26', title: '', body: '', author: { type: 'user', id: '1' }, tags: [{ type: 'tag', id: '11' }] },
				{ id: '0', title: '', body: '', author: { type: 'user', id: '1' }, tags: [] }
			],

			tag: [
				{ id: '10', name: '10' },
				{ id: '11', name: '11' }
			]
		};

		deepEqual(serializer.deserialize(payload), expected);
	});

	test('Top-level and linked objects are all deserialized', function() {
		expect(1);

		var payload = {
			tags: [{ id: '1', name: 'One' }],
			linked: {
				tags: [{ id: '2', name: 'Two' }]
			}
		};

		var expected = {
			meta: {
				serverMeta: {}
			},
			tag: [
				{ id: '1', name: 'One' },
				{ id: '2', name: 'Two' }
			]
		};

		deepEqual(serializer.deserialize(payload, {}), expected);
	});
})();

<|start_filename|>test/model/relationship/reconnect.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Relationship Reconnection Test', {
		setup: function() {
			store = setupStore({
				vertex: EG.Model.extend({
					parent: EG.hasOne({
						relatedType: 'vertex',
						inverse: 'children'
					}),
					children: EG.hasMany({
						relatedType: 'vertex',
						inverse: 'parent'
					})
				})
			}, {
				adapter: EG.MemoryAdapter.extend({
					shouldInitializeDatabase: function() {
						return true;
					},
					getInitialPayload: function() {
						return {
							vertex: [
								{
									id: '1',
									parent: null,
									children: [
										{ type: 'vertex', id: '2' },
										{ type: 'vertex', id: '3' }
									]
								},
								{
									id: '2',
									parent: { type: 'vertex', id: '1' },
									children: []
								},
								{
									id: '3',
									parent: { type: 'vertex', id: '1' },
									children: []
								}
							]
						};
					}
				})
			});
		}
	});

	asyncTest('Switch parent vertices (reloading the same relationship', 1, function() {
		store.find('vertex').then(function() {
			var vertex1 = store.getRecord('vertex', '1');
			var vertex2 = store.getRecord('vertex', '2');
			var vertex3 = store.getRecord('vertex', '3');

			vertex3.setHasOneRelationship('parent', vertex2);
			vertex3.save().then(function() {
				vertex3.setHasOneRelationship('parent', vertex1);
				return vertex3.save();
			}).then(function() {
				start();
				ok(true);
			}, function(e) {
				start();
				throw e;
			});
		});
	});
})();

<|start_filename|>src/attribute_type/array.js<|end_filename|>
import Ember from 'ember';
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * @class ArrayType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * If the object is an array, it's returned. Otherwise, `null` is returned.
	 * This doesn't check the individual elements, just the array.
	 *
	 * @method serialize
	 * @param {Array} arr
	 * @returns {Array}
	 */
	serialize: function(arr) {
		if (Ember.isNone(arr)) {
			return null;
		}

		return (Ember.isArray(arr.toArray ? arr.toArray() : arr) ? arr : null);
	},

	/**
	 * If the object is an array, it's returned. Otherwise, `null` is returned.
	 * This doesn't check the individual elements, just the array.
	 *
	 * @method deserialize
	 * @param {Array} arr
	 * @returns {Array}
	 */
	deserialize: function(arr) {
		return (Ember.isArray(arr) ? arr : null);
	},

	/**
	 * Compares two arrays using `Ember.compare`.
	 *
	 * @method isEqual
	 * @param {Array} a
	 * @param {Array} b
	 * @returns {Boolean}
	 */
	isEqual: function(a, b) {
		if (!Ember.isArray(a) || !Ember.isArray(b)) {
			return false;
		}

		return Ember.compare(a.toArray(), b.toArray()) === 0;
	}
});

<|start_filename|>src/shim.js<|end_filename|>
import EmberGraph from 'ember-graph';

// Data Adapter
import DataAdapter from 'ember-graph/util/data_adapter';
EmberGraph.DataAdapter = DataAdapter;

// Array polyfills
import {
	some,
	reduce,
	mapBy
} from 'ember-graph/util/array';
EmberGraph.ArrayPolyfills = {
	some: some,
	reduce: reduce,
	mapBy: mapBy
};

// EmberGraph namespace methods
import {
	abstractMethod,
	abstractProperty,
	generateUUID,
	arrayContentsEqual,
	groupRecords,
	values,
	deprecateMethod,
	deprecateProperty
} from 'ember-graph/util/util';
EmberGraph.abstractMethod = abstractMethod;
EmberGraph.abstractProperty = abstractProperty;
EmberGraph.generateUUID = generateUUID;
EmberGraph.arrayContentsEqual = arrayContentsEqual;
EmberGraph.groupRecords = groupRecords;
EmberGraph.values = values;
EmberGraph.deprecateMethod = deprecateMethod;
EmberGraph.deprecateProperty = deprecateProperty;

import {
	attr,
	hasOne,
	hasMany
} from 'ember-graph/model/schema';
EmberGraph.attr = attr;
EmberGraph.hasOne = hasOne;
EmberGraph.hasMany = hasMany;

// Set
import EmberGraphSet from 'ember-graph/util/set';
EmberGraph.Set = EmberGraphSet;

// String polyfills
import {
	startsWith,
	endsWith,
	capitalize,
	decapitalize
} from 'ember-graph/util/string';
import {
	pluralize,
	singularize
} from 'ember-graph/util/inflector';
EmberGraph.String = {
	startsWith: startsWith,
	endsWith: endsWith,
	capitalize: capitalize,
	decapitalize: decapitalize,
	pluralize: pluralize,
	singularize: singularize
};

// Promise proxy objects
import {
	PromiseObject,
	PromiseArray,
	ModelPromiseObject
} from 'ember-graph/data/promise_object';
EmberGraph.PromiseObject = PromiseObject;
EmberGraph.PromiseArray = PromiseArray;
EmberGraph.ModelPromiseObject = ModelPromiseObject;

// Serializers
import Serializer from 'ember-graph/serializer/serializer';
EmberGraph.Serializer = Serializer;
import JSONSerializer from 'ember-graph/serializer/json';
EmberGraph.JSONSerializer = JSONSerializer;
import EmberGraphSerializer from 'ember-graph/serializer/ember_graph';
EmberGraph.EmberGraphSerializer = EmberGraphSerializer;

// Adapters
import Adapter from 'ember-graph/adapter/adapter';
EmberGraph.Adapter = Adapter;
import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter';
EmberGraph.EmberGraphAdapter = EmberGraphAdapter;
import LocalStorageAdapter from 'ember-graph/adapter/local_storage';
EmberGraph.LocalStorageAdapter = LocalStorageAdapter;
import MemoryAdapter from 'ember-graph/adapter/memory';
EmberGraph.MemoryAdapter = MemoryAdapter;
import RESTAdapter from 'ember-graph/adapter/rest';
EmberGraph.RESTAdapter = RESTAdapter;

// Store
import Store from 'ember-graph/store/store';
EmberGraph.Store = Store;

// Attribute Types
import AttributeType from 'ember-graph/attribute_type/type';
EmberGraph.AttributeType = AttributeType;
import ArrayType from 'ember-graph/attribute_type/array';
EmberGraph.ArrayType = ArrayType;
import BooleanType from 'ember-graph/attribute_type/boolean';
EmberGraph.BooleanType = BooleanType;
import DateType from 'ember-graph/attribute_type/date';
EmberGraph.DateType = DateType;
import EnumType from 'ember-graph/attribute_type/enum';
EmberGraph.EnumType = EnumType;
import NumberType from 'ember-graph/attribute_type/number';
EmberGraph.NumberType = NumberType;
import ObjectType from 'ember-graph/attribute_type/object';
EmberGraph.ObjectType = ObjectType;
import StringType from 'ember-graph/attribute_type/string';
EmberGraph.StringType = StringType;

// Model
import Model from 'ember-graph/model/model';
EmberGraph.Model = Model;

// Testing shims
import RelationshipHash from 'ember-graph/relationship/relationship_hash';
EmberGraph.RelationshipHash = RelationshipHash;
import Relationship from 'ember-graph/relationship/relationship';
EmberGraph.Relationship = Relationship;
import RelationshipStore from 'ember-graph/relationship/relationship_store';
EmberGraph.RelationshipStore = RelationshipStore;
import RecordCache from 'ember-graph/store/record_cache';
EmberGraph.RecordCache = RecordCache;
import RecordRequestCache from 'ember-graph/store/record_request_cache';
EmberGraph.RecordRequestCache = RecordRequestCache;

// Inflector
import {
	overridePluralRule,
	overrideSingularRule
} from 'ember-graph/util/inflector';
EmberGraph.Inflector = { singularize, pluralize, overridePluralRule, overrideSingularRule };

<|start_filename|>test/store/store.js<|end_filename|>
(function() {
	'use strict';

	var typeKey = 'storeTest';

	var store;
	var records;

	var Adapter = EG.Adapter.extend({

		createRecord: function(record) {
			var id = EG.generateUUID();
			return Em.RSVP.Promise.resolve({
				meta: {
					createdRecord: {
						type: record.get('typeKey'),
						id: id
					}
				},
				storeTest: [{
					id: id
				}]
			});
		},

		findRecord: function(type, id) {
			return Em.RSVP.Promise.resolve({ storeTest: [records[id]] });
		},

		findMany: function(type, ids) {
			return Em.RSVP.Promise.resolve({
				storeTest: ids.map(function(id) {
					return records[id];
				})
			});
		},

		findAll: function(type) {
			return this.findMany(type, ['1', '2', '3', '4']);
		},

		updateRecord: function(record) {
			return Em.RSVP.Promise.resolve({ storeTest: [records[record.get('id')]] });
		},

		deleteRecord: function(record) {
			return Em.RSVP.Promise.resolve({});
		}
	});

	module('Store Test', {
		setup: function() {
			store = setupStore({
				storeTest: EG.Model.extend(),

				user: EG.Model.extend({
					name: EG.attr({
						type: 'string'
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author'
					})
				}),

				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts'
					}),

					posted: EG.attr({
						type: 'date',
						serverOnly: true
					})
				})
			}, { adapter: Adapter });

			records = {
				'1': { id: '1' },
				'2': { id: '2' },
				'3': { id: '3' },
				'4': { id: '4' }
			};
		}
	});

	test('The store can load records properly', function() {
		expect(6);

		strictEqual(store.getRecord(typeKey, '1'), null);
		strictEqual(store.getRecord(typeKey, '2'), null);
		strictEqual(store.getRecord(typeKey, '4'), null);

		var payload = {};
		payload[typeKey] = [records[1], records[2], records[4]];
		store.pushPayload(payload);

		ok(store.getRecord(typeKey, '1'));
		ok(store.getRecord(typeKey, '2'));
		ok(store.getRecord(typeKey, '4'));
	});

	asyncTest('The store can find a single record properly', function() {
		expect(3);

		strictEqual(store.getRecord(typeKey, '1'), null);
		store.find(typeKey, '1').then(function(record) {
			start();
			ok(store.getRecord(typeKey, '1'));
			strictEqual(record.get('id'), '1');
		});
	});

	asyncTest('The store can load and find multiple records properly', function() {
		expect(4);

		store.find(typeKey, ['1', '2', '4']).then(function(resolvedRecords) {
			start();

			var set = EG.Set.create();
			set.addObjects(resolvedRecords);
			ok(Em.get(set, 'length') === 3);
			ok(store.getRecord(typeKey, '1'));
			ok(store.getRecord(typeKey, '2'));
			ok(store.getRecord(typeKey, '4'));
		});
	});

	asyncTest('The store can find all records of a type properly', function() {
		expect(5);

		store.find(typeKey).then(function(resolvedRecords) {
			start();

			var set = EG.Set.create();
			set.addObjects(resolvedRecords);

			ok(Em.get(set, 'length') === 4);
			ok(store.getRecord(typeKey, '1'));
			ok(store.getRecord(typeKey, '2'));
			ok(store.getRecord(typeKey, '3'));
			ok(store.getRecord(typeKey, '4'));
		});
	});

	asyncTest('The store saves new records properly', function() {
		expect(4);

		var record = store.createRecord(typeKey, {});
		var tempId = record.get('id');

		ok(store.getRecord(typeKey, tempId));

		var promise = store.saveRecord(record);

		promise.then(function() {
			start();

			ok(!store.getRecord(typeKey, tempId));
			ok(tempId !== record.get('id'));
			ok(store.getRecord(typeKey, record.get('id')));
		});
	});

	asyncTest('The store deletes a record properly', function() {
		expect(3);

		var record;

		store.find(typeKey, '1').then(function(r) {
			record = r;
			var promise = record.destroy();

			start();
			strictEqual(record.get('isDeleting'), true);
			stop();

			return promise;
		}).then(function() {
			start();

			strictEqual(store.getRecord(typeKey, '1'), null);
			strictEqual(record.get('isDeleted'), true);
		});
	});

	test('The `cacheFor` method functions properly', function() {
		expect(4);

		var payload = {};

		payload[typeKey] = [records[1]];
		store.pushPayload(payload);
		deepEqual(store.cachedRecordsFor(typeKey).mapBy('id').sort(), ['1'].sort());

		payload[typeKey] = [records[2]];
		store.pushPayload(payload);
		deepEqual(store.cachedRecordsFor(typeKey).mapBy('id').sort(), ['1', '2'].sort());

		payload[typeKey] = [records[3]];
		store.pushPayload(payload);
		deepEqual(store.cachedRecordsFor(typeKey).mapBy('id').sort(), ['1', '2', '3'].sort());

		payload[typeKey] = [records[4]];
		store.pushPayload(payload);
		deepEqual(store.cachedRecordsFor(typeKey).mapBy('id').sort(), ['1', '2', '3', '4'].sort());
	});

	test('Look up per-type adapters', function() {
		expect(3);

		var UserAdapter = EG.Adapter.extend({});
		var ForumAdminAdapter = EG.Adapter.extend({});
		store.__registry__.register('adapter:user', UserAdapter);
		store.__registry__.register('adapter:forum_admin', ForumAdminAdapter);

		ok(UserAdapter.detectInstance(store.adapterFor('user')));
		ok(ForumAdminAdapter.detectInstance(store.adapterFor('forum_admin')));
		ok(Adapter.detectInstance(store.adapterFor('foobar')));
	});

	test('Can\'t save a record with uninitialized attributes', function() {
		expect(2);

		var user = store.createRecord('user', { posts: [] });
		strictEqual(user.isAttributeInitialized('name'), false);

		throws(function() {
			user.save();
		});
	});

	test('Can\'t save record with uninitialized relationships', function() {
		expect(2);

		var user = store.createRecord('user', { name: '' });
		strictEqual(user.isRelationshipInitialized('posts'), false);

		throws(function() {
			user.save();
		});
	});

	test('Can save record with uninitialized serverOnly attribute', function() {
		expect(1);

		var post = store.createRecord('post', { author: '1' });
		strictEqual(post.isAttributeInitialized('posted'), false);
		post.save().catch(function() {});
	});

	test('Looking up non-existent model class throws error', function() {
		expect(1);

		throws(function() {
			store.modelFor('posts');
		}, /typeKey/gi);
	});
})();


<|start_filename|>src/util/compatibility.js<|end_filename|>
import Ember from 'ember';

/**
 * This function will return `true` if the current version of
 * Ember is at least the version number specified. If not,
 * it will return false.
 *
 * @param {Number} major
 * @param {Number} minor
 * @param {Number} patch
 * @return {Boolean}
 */
function verifyAtLeastEmberVersion(major, minor, patch) {
	const emberVersionParts = Ember.VERSION.split(/\.|\-/);
	const emberVersionNumbers = emberVersionParts.map((part) => parseInt(part, 10));

	if (emberVersionNumbers[0] < major) {
		return false;
	} else if (emberVersionNumbers[0] > major) {
		return true;
	}

	if (emberVersionNumbers[1] < minor) {
		return false;
	} else if (emberVersionNumbers[1] > minor) {
		return true;
	}

	if (emberVersionNumbers[2] < patch) {
		return false;
	}

	return true;
}

export { verifyAtLeastEmberVersion };

<|start_filename|>src/util/data_adapter.js<|end_filename|>
import Ember from 'ember';
import Model from 'ember-graph/model/model';

import { computed } from 'ember-graph/util/computed';


/**
 * Extends Ember's `DataAdapter` class to provide debug functionality for the Ember Inspector.
 *
 * Thanks to the Ember-Data team for the reference implementation.
 *
 * @class DataAdapter
 * @private
 */
const EmberGraphDataAdapter = Ember.DataAdapter && Ember.DataAdapter.extend({

	containerDebugAdapter: computed({
		get() {
			return Ember.getOwner(this).lookup('container-debug-adapter:main');
		}
	}),

	getFilters: function() {
		return [
			{ name: 'isNew', desc: 'New' },
			{ name: 'isModified', desc: 'Modified' },
			{ name: 'isClean', desc: 'Clean' }
		];
	},

	detect: function(modelClass) {
		return (modelClass !== Model && Model.detect(modelClass));
	},

	columnsForType: function(modelClass) {
		var attributeLimit = this.get('attributeLimit');
		var columns = [{ name: 'id', desc: 'Id' }];

		modelClass.eachAttribute(function(name, meta) {
			if (columns.length > attributeLimit) {
				return;
			}

			var desc = Ember.String.capitalize(Ember.String.underscore(name).replace(/_/g, ' '));
			columns.push({ name: name, desc: desc });
		});

		return columns;
	},

	getRecords: function(modelClass) {
		var typeKey = Ember.get(modelClass, 'typeKey');
		return this.get('store').getLiveRecordArray(typeKey);
	},

	getRecordColumnValues: function(record) {
		var values = { id: record.get('id') };

		record.constructor.eachAttribute(function(name, meta) {
			values[name] = record.get(name);
		});

		return values;
	},

	getRecordKeywords: function(record) {
		var keywords = [];

		record.constructor.eachAttribute(function(name) {
			keywords.push(record.get(name) + '');
		});

		return keywords;
	},

	getRecordFilterValues: function(record) {
		var isNew = record.get('isNew');
		var isDirty = record.get('isDirty');

		return {
			isNew: isNew,
			isModified: isDirty && !isNew,
			isClean: !isDirty
		};
	},

	getRecordColor: function(record) {
		if (record.get('isNew')) {
			return 'green';
		} else if (record.get('isDirty')) {
			return 'blue';
		} else {
			return 'black';
		}
	},

	observeRecord: function(record, recordUpdated) {
		var releaseMethods = Ember.A();
		var propertiesToObserve = Ember.A(['id', 'isNew', 'isDirty']);

		propertiesToObserve.addObjects(Ember.get(record.constructor, 'attributes').toArray());

		propertiesToObserve.forEach((name) => {
			var handler = () => this.wrapRecord(record);

			Ember.addObserver(record, name, handler);

			releaseMethods.push(() => Ember.removeObserver(record, name, handler));
		});

		return function() {
			releaseMethods.forEach((release) => release());
		};
	}
});

const DataAdapter = (Ember.DataAdapter ? EmberGraphDataAdapter : null);

export default DataAdapter;


<|start_filename|>tasks/transpile.js<|end_filename|>
'use strict';

var fs = require('fs');
var path = require('path');
var babel = require('babel');
var readdir = require('fs-readdir-recursive');

var SOURCE_DIRECTORY = path.resolve(process.cwd() + '/src');
var DESTINATION_DIRECTORY = path.resolve(process.cwd() + '/dist');
var OUTPUT_FILE_PATH = path.resolve(process.cwd() + '/dist/ember-graph.js');
var BEFORE_LOAD_SCRIPT = path.resolve(process.cwd() + '/src/before_load.js');
var AFTER_LOAD_SCRIPT = path.resolve(process.cwd() + '/src/after_load.js');

var FILE_CACHE = {};
var LAST_TRANSPILE = 0;

module.exports = function(grunt) {
	grunt.registerTask('transpile', transpile);
};

function transpile() {
	console.log('Transpiling...');
	var startTime = Date.now();
	buildOutput();
	LAST_TRANSPILE = Date.now();
	var timeElapsed = LAST_TRANSPILE - startTime;
	console.log('...done. Finished in ' + Math.ceil(timeElapsed / 1000) + ' seconds');
}

function buildOutput() {
	try {
		fs.mkdirSync(DESTINATION_DIRECTORY);
	} catch (e) {
		if (e.code !== 'EEXIST') {
			throw e;
		}
	}

	transpileSources();
	cleanCache();
	concatenateFiles();
}

function transpileSources() {
	var filePaths = scanForFiles();
	var changedFiles = filterUnchangedFiles(filePaths);
	changedFiles.forEach(function(filePath) {
		console.log('Transpiling file: ' + path.relative(SOURCE_DIRECTORY, filePath));
		FILE_CACHE[filePath] = transpileFile(filePath);
	});
}

function cleanCache() {
	Object.keys(FILE_CACHE).forEach(function(filePath) {
		if (!fs.existsSync(filePath)) {
			delete FILE_CACHE[filePath];
		}
	});
}

function concatenateFiles() {
	fs.writeFileSync(OUTPUT_FILE_PATH, '(function() {\n\'use strict\';\n\n');

	var beforeLoadScriptContents = fs.readFileSync(BEFORE_LOAD_SCRIPT, 'utf8');
	fs.appendFileSync(OUTPUT_FILE_PATH, beforeLoadScriptContents);

	Object.keys(FILE_CACHE).forEach(function(filePath) {
		fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n');
		fs.appendFileSync(OUTPUT_FILE_PATH, FILE_CACHE[filePath]);
	});

	fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n');
	var afterLoadScriptContents = fs.readFileSync(AFTER_LOAD_SCRIPT, 'utf8');
	fs.appendFileSync(OUTPUT_FILE_PATH, afterLoadScriptContents);

	fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n}).call(window || this);');
}

function scanForFiles() {
	return readdir(SOURCE_DIRECTORY).map(function(name) {
		return path.resolve(SOURCE_DIRECTORY + '/' + name);
	}).filter(function(name) {
		if (!endsWith(name, '.js')) {
			return false;
		}

		if (name === BEFORE_LOAD_SCRIPT) {
			return false;
		}

		if (name === AFTER_LOAD_SCRIPT) {
			return false;
		}

		return true;
	});
}

function filterUnchangedFiles(filePaths) {
	return filePaths.filter(function(filePath) {
		var stat = fs.statSync(filePath);
		return (stat.mtime.getTime() > LAST_TRANSPILE);
	});
}

function transpileFile(filePath) {
	var fileContents = fs.readFileSync(filePath, 'utf8');
	var options = {
		filename: path.relative(SOURCE_DIRECTORY, filePath),
		nonStandard: false,
		sourceRoot: SOURCE_DIRECTORY,
		moduleRoot: 'ember-graph',
		modules: 'amdStrict',
		moduleIds: true,
		whitelist: [
			'es6.arrowFunctions',
			'es6.blockScoping',
			'es6.constants',
			'es6.destructuring',
			'es6.modules',
			'es6.parameters',
			'es6.properties.computed',
			'es6.properties.shorthand',
			'es6.spread',
			'es6.templateLiterals'
		]
	};

	return babel.transform(fileContents, options).code;
}

function endsWith(string, suffix) {
	return string.indexOf(suffix, string.length - suffix.length) >= 0;
}

<|start_filename|>src/adapter/ember_graph/database.js<|end_filename|>
import Ember from 'ember';
import Model from 'ember-graph/model/model';
import copy from 'ember-graph/util/copy';

import { abstractMethod } from 'ember-graph/util/util';
import { startsWith } from 'ember-graph/util/string';

var ADD_OP_NAME_REGEX = /^\/links\/([^/]+)/i;
var REMOVE_OP_REGEX = /^\/links\/([^/]+)\/.+/i;

function getRelationshipNameFromChangePath(path, op) {
	return path.match(op === 'add' ? ADD_OP_NAME_REGEX : REMOVE_OP_REGEX)[1];
}

/**
 * The JSON database has a simple format. The top-level
 * format looks like this:
 *
 * ```json
 * {
 *     "records": {},
 *     "relationships": []
 * }
 * ```
 *
 * Records are grouped by their type and stored by ID, like so:
 *
 * ```json
 * {
 *     "user": {
 *         "1": {},
 *         "3": {}
 *     },
 *     "post": {
 *         "10": {}
 *     }
 * }
 * ```
 *
 * Relationships are stored in the following form (short keys are to conserve space):
 *
 * ```json
 * {
 *    "t1": "typeKey1",
 *    "i1": "id1",
 *    "n1": "relationshipName1",
 *    "t2": "typeKey2",
 *    "i2": "id2",
 *    "n2": "relationshipName2"
 * }
 * ```
 */
export default {

	/**
	 * Return a copy of the database from the storage location in JSON form.
	 * If the database doesn't exist and you need to create and empty one,
	 * the following JSON object should be returned:
	 *
	 * ```json
	 * {
	 *     "records": {},
	 *     "relationships": []
	 * }
	 * ```
	 *
	 * @method getDatabase
	 * @return {Promise} Resolves to the DB JSON
	 * @protected
	 * @for EmberGraphAdapter
	 */
	getDatabase: abstractMethod('getDatabase'),

	/**
	 * Store the updated version of the database in the storage location.
	 *
	 * @method setDatabase
	 * @param {JSON} db
	 * @return {Promise} Resolves or rejects based on saving success (resolves to current DB)
	 * @protected
	 * @for EmberGraphAdapter
	 */
	setDatabase: abstractMethod('saveDatabase'),

	/**
	 * Determines if the given database contains the given record.
	 *
	 * @method databaseHasRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {JSON} db
	 * @return {Boolean}
	 * @private
	 * @for EmberGraphAdapter
	 */
	databaseHasRecord: function(typeKey, id, db) {
		try {
			return (!!db.records[typeKey][id]);
		} catch (e) {
			return false;
		}
	},

	/**
	 * Builds the record from the database, combining the relationships and attributes.
	 * This assumes that the record actually exists.
	 *
	 * @method getRecordFromDatabase
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {JSON} db
	 * @return {JSON}
	 * @private
	 * @for EmberGraphAdapter
	 */
	getRecordFromDatabase: function(typeKey, id, db) {
		var model = this.get('store').modelFor(typeKey);
		var json = copy(db.records[typeKey][id], true);
		json.id = id;
		json.links = {};

		db.relationships.forEach(function(relationship) {
			var meta;

			if (relationship.t1 === typeKey && relationship.i1 === id && relationship.n1 !== null) {
				meta = model.metaForRelationship(relationship.n1);

				if (meta.kind === Model.HAS_ONE_KEY) {
					json.links[relationship.n1] = { type: relationship.t2, id: relationship.i2 };
				} else {
					json.links[relationship.n1] = json.links[relationship.n1] || [];
					json.links[relationship.n1].push({ type: relationship.t2, id: relationship.i2 });
				}
			} else if (relationship.t2 === typeKey && relationship.i2 === id && relationship.n2 !== null) {
				meta = model.metaForRelationship(relationship.n2);

				if (meta.kind === Model.HAS_ONE_KEY) {
					json.links[relationship.n2] = { type: relationship.t1, id: relationship.i1 };
				} else {
					json.links[relationship.n2] = json.links[relationship.n2] || [];
					json.links[relationship.n2].push({ type: relationship.t1, id: relationship.i1 });
				}
			}
		});

		model.eachRelationship(function(name, meta) {
			if (!json.links[name]) {
				if (meta.kind === Model.HAS_ONE_KEY) {
					json.links[name] = null;
				} else {
					json.links[name] = [];
				}
			}
		});

		return json;
	},

	/**
	 * Takes a serialized record and splits it into attributes
	 * and relationships, then puts it in the database. It will
	 * replace any existing record with the same type and ID.
	 *
	 * @method putRecordInDatabase
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {JSON} json
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	putRecordInDatabase: function(typeKey, id, json, db) {
		var model = this.get('store').modelFor(typeKey);

		db.records[typeKey] = db.records[typeKey] || {};
		db.records[typeKey][id] = {};

		model.eachAttribute(function(name, meta) {
			db.records[typeKey][id][name] = json[name];
		});

		model.eachRelationship(function(name, meta) {
			if (meta.kind === Model.HAS_ONE_KEY) {
				if (json.links[name]) {
					var relationship = {
						t1: typeKey, i1: id, n1: name,
						t2: json.links[name].type, i2: json.links[name].id, n2: meta.inverse
					};

					db = this.setHasOneRelationshipInDatabase(relationship, db);
				}
			} else {
				json.links[name].forEach(function(value) {
					var relationship = {
						t1: typeKey, i1: id, n1: name,
						t2: value.type, i2: value.id, n2: meta.inverse
					};

					db = this.addHasManyRelationshipToDatabase(relationship, db);
				}, this);
			}
		}, this);

		return db;
	},

	/**
	 * Applies a list of changes for a given record to the database.
	 *
	 * @method applyChangesToDatabase
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {JSON[]} changes
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	applyChangesToDatabase: function(typeKey, id, changes, db) {
		var model = this.get('store').modelFor(typeKey);

		changes.forEach(function(change) {
			switch (change.op) {
				case 'replace':
					if (startsWith(change.path, '/links/')) {
						var hasOneName = change.path.substring('/links/'.length);

						if (change.value === null) {
							db = this.clearHasOneRelationshipInDatabase(typeKey, id, hasOneName, db);
						} else {
							var hasOneMeta = model.metaForRelationship(hasOneName);

							var replacementRelationship = {
								t1: typeKey, i1: id, n1: hasOneName,
								t2: change.value.type, i2: change.value.id, n2: hasOneMeta.inverse
							};

							db = this.setHasOneRelationshipInDatabase(replacementRelationship, db);
						}
					} else {
						var attrName = change.path.substring('/'.length);
						db.records[typeKey][id][attrName] = change.value;
					}
					break;
				case 'add':
				case 'remove':
					var hasManyName = getRelationshipNameFromChangePath(change.path, change.op);
					var hasManyMeta = model.metaForRelationship(hasManyName);

					var relationship = {
						t1: typeKey, i1: id, n1: hasManyName,
						t2: change.value.type, i2: change.value.id, n2: hasManyMeta.inverse
					};

					if (change.op === 'add') {
						db = this.addHasManyRelationshipToDatabase(relationship, db);
					} else {
						db = this.removeHasManyRelationshipFromDatabase(relationship, db);
					}
					break;
			}
		}, this);

		return db;
	},

	/**
	 * Adds a new hasMany relationship to the database, removing any conflicts.
	 * The hasMany relationship should be the first one in the relationship JSON.
	 *
	 * @method addHasManyRelationshipToDatabase
	 * @param {JSON} relationship
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	addHasManyRelationshipToDatabase: function(relationship, db) {
		var relationships = this.getRelationshipsFor(relationship.t1, relationship.i1, relationship.n1, db);

		var connected = relationships.filter(function(r) {
			return (relationship.t2 === r.t2 && relationship.i2 === r.i2 && relationship.n2 === r.n2);
		});

		if (connected.length > 0) {
			return db;
		}

		if (relationship.n2) {
			var inverseModel = this.get('store').modelFor(relationship.t2);
			var inverseMeta = inverseModel.metaForRelationship(relationship.n2);

			if (inverseMeta.kind === Model.HAS_ONE_KEY) {
				db = this.clearHasOneRelationshipInDatabase(relationship.t2, relationship.i2, relationship.n2, db);
			}
		}

		db.relationships.push(relationship);

		return db;
	},

	/**
	 * Removes a hasMany relationship from the database.
	 * The hasMany relationship should be the first one in the relationship JSON.
	 *
	 * @method removeHasManyRelationshipFromDatabase
	 * @param {JSON} relationship
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	removeHasManyRelationshipFromDatabase: function(relationship, db) {
		db.relationships = db.relationships.filter(function(r) {
			return !(relationship.t2 === r.t2 && relationship.i2 === r.i2 && relationship.n2 === r.n2);
		});

		return db;
	},

	/**
	 * Sets a hasOne relationship to a new value, removing any conflicts.
	 * The hasOne relationship should be the first one in the relationship JSON.
	 *
	 * @method setHasOneRelationshipInDatabase
	 * @param {JSON} relationship
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	setHasOneRelationshipInDatabase: function(relationship, db) {
		db = this.clearHasOneRelationshipInDatabase(relationship.t1, relationship.i1, relationship.n1, db);

		if (relationship.n2) {
			var inverseModel = this.get('store').modelFor(relationship.t2);
			var inverseMeta = inverseModel.metaForRelationship(relationship.n2);

			if (inverseMeta.kind === Model.HAS_ONE_KEY) {
				db = this.clearHasOneRelationshipInDatabase(relationship.t2, relationship.i2, relationship.n2, db);
			}
		}

		db.relationships.push(relationship);

		return db;
	},

	/**
	 * Clears the value of the given hasOne relationship (if there is one).
	 *
	 * @method clearHasOneRelationshipInDatabase
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {String} name
	 * @param {JSON} db
	 * @return {JSON} The updated DB
	 * @private
	 * @for EmberGraphAdapter
	 */
	clearHasOneRelationshipInDatabase: function(typeKey, id, name, db) {
		var relationships = this.getRelationshipsFor(typeKey, id, name, db);

		relationships.forEach(function(relationship) {
			db.relationships.splice(db.relationships.indexOf(relationship), 1);
		});

		return db;
	},

	/**
	 * Gets all of the relationships that connect to the record given.
	 *
	 * @method getRelationshipsFor
	 * @param {String} typeKey
	 * @param {String} id
	 * @param {String} name
	 * @param {JSON} db
	 * @return {JSON[]} Relationships
	 * @private
	 * @for EmberGraphAdapter
	 */
	getRelationshipsFor: function(typeKey, id, name, db) {
		return db.relationships.filter(function(relationship) {
			return ((relationship.t1 === typeKey && relationship.i1 === id && relationship.n1 === name) ||
				(relationship.t2 === typeKey && relationship.i2 === id && relationship.n2 === name));
		});
	}

};


<|start_filename|>test/model/relationship/reloading.js<|end_filename|>
(function() {

	var store;

	module('Relationship Reload Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author'
					})
				}),
				post: EG.Model.extend({
					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts'
					})
				})
			});

			store.pushPayload({
				user: [
					{
						id: '1',
						posts: [
							{ type: 'post', id: '1' },
							{ type: 'post', id: '2' },
							{ type: 'post', id: '3' }
						]
					}
				],
				post: [
					{ id: '1', author: { type: 'user', id: '1' } },
					{ id: '2', author: { type: 'user', id: '1' } },
					{ id: '3', author: { type: 'user', id: '1' } }
				]
			});
		}
	});

	asyncTest('Underscore observer fires when new relationships are added', function() {
		expect(1);

		var model = store.getRecord('user', '1');
		model.get('_posts');

		model.addObserver('_posts', model, function() {
			start();
			ok(true);
		});

		store.pushPayload({
			user: [{
				id: '1',
				posts: [
					{ type: 'post', id: '1' },
					{ type: 'post', id: '2' },
					{ type: 'post', id: '3' },
					{ type: 'post', id: '4' }
				]
			}],
			post: [
				{ id: '4', author: { type: 'user', id: '1' } }
			]
		});
	});

	asyncTest('Relationship observer fires when new relationships are added', function() {
		expect(1);

		var model = store.getRecord('user', '1');
		model.get('posts');

		model.addObserver('posts', model, function() {
			start();
			ok(true);
		});

		store.pushPayload({
			user: [{
				id: '1',
				posts: [
					{ type: 'post', id: '1' },
					{ type: 'post', id: '2' },
					{ type: 'post', id: '3' },
					{ type: 'post', id: '4' }
				]
			}],
			post: [
				{ id: '4', author: { type: 'user', id: '1' } }
			]
		});
	});
})();

<|start_filename|>test/model/states.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('Model State Test', {
		setup: function() {
			store = setupStore({
				tag: EG.Model.extend({
					name: EG.attr({
						type: 'string',
						defaultValue: '(none)'
					})
				})
			}, {
				adapter: EG.Adapter.extend({
					findRecord: function() {
						return Em.RSVP.Promise.resolve({});
					},

					createRecord: function(record) {
						var id = EG.generateUUID();

						return Em.RSVP.Promise.resolve({
							meta: {
								createdRecord: {
									type: record.get('typeKey'),
									id: id
								}
							},
							tag: [{
								id: id, name: record.get('name')
							}]
						});
					},

					updateRecord: function(record) {
						return Em.RSVP.Promise.resolve({
							tag: [{
								id: record.get('id'),
								name: record.get('name')
							}]
						});
					},

					deleteRecord: function(record) {
						return Em.RSVP.Promise.resolve({});
					}
				})
			});

			store.pushPayload({
				tag: [
					{ id: '1', name: 'c++' },
					{ id: '2', name: 'pizza' },
					{ id: '3', name: 'ember' },
					{ id: '4', name: 'rubik\'s cube' }
				]
			});
		}
	});

	asyncTest('Creating a record', function() {
		expect(27);

		var tag = store.createRecord('tag');

		start();
		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), true);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), true);
		strictEqual(tag.get('isInTransit'), false);

		tag.save().then(function() {
			start();
			strictEqual(tag.get('isDeleting'), false);
			strictEqual(tag.get('isDeleted'), false);
			strictEqual(tag.get('isSaving'), false);
			strictEqual(tag.get('isReloading'), false);
			strictEqual(tag.get('isLoaded'), true);
			strictEqual(tag.get('isDirty'), false);
			strictEqual(tag.get('isCreating'), false);
			strictEqual(tag.get('isNew'), false);
			strictEqual(tag.get('isInTransit'), false);

		});

		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), true);
		strictEqual(tag.get('isCreating'), true);
		strictEqual(tag.get('isNew'), true);
		strictEqual(tag.get('isInTransit'), true);
		stop();
	});

	asyncTest('Saving a record', function() {
		expect(28);

		var tag = store.getRecord('tag', '1');

		start();
		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), false);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), false);

		tag.set('name', 'foobar');

		strictEqual(tag.get('isDirty'), true);

		tag.save().then(function() {
			start();
			strictEqual(tag.get('isDeleting'), false);
			strictEqual(tag.get('isDeleted'), false);
			strictEqual(tag.get('isSaving'), false);
			strictEqual(tag.get('isReloading'), false);
			strictEqual(tag.get('isLoaded'), true);
			strictEqual(tag.get('isDirty'), false);
			strictEqual(tag.get('isCreating'), false);
			strictEqual(tag.get('isNew'), false);
			strictEqual(tag.get('isInTransit'), false);

		});

		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), true);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), true);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), true);
		stop();
	});

	asyncTest('Reloading a record', function() {
		expect(27);

		var tag = store.getRecord('tag', '1');

		start();
		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), false);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), false);

		tag.reload().then(function() {
			start();
			strictEqual(tag.get('isDeleting'), false);
			strictEqual(tag.get('isDeleted'), false);
			strictEqual(tag.get('isSaving'), false);
			strictEqual(tag.get('isReloading'), false);
			strictEqual(tag.get('isLoaded'), true);
			strictEqual(tag.get('isDirty'), false);
			strictEqual(tag.get('isCreating'), false);
			strictEqual(tag.get('isNew'), false);
			strictEqual(tag.get('isInTransit'), false);

		});

		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), true);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), false);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), true);
		stop();
	});

	asyncTest('Deleting a record', function() {
		expect(27);

		var tag = store.getRecord('tag', '1');

		start();
		strictEqual(tag.get('isDeleting'), false);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), false);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), false);

		tag.destroy().then(function() {
			start();
			strictEqual(tag.get('isDeleting'), false);
			strictEqual(tag.get('isDeleted'), true);
			strictEqual(tag.get('isSaving'), false);
			strictEqual(tag.get('isReloading'), false);
			strictEqual(tag.get('isLoaded'), false);
			strictEqual(tag.get('isDirty'), false);
			strictEqual(tag.get('isCreating'), false);
			strictEqual(tag.get('isNew'), false);
			strictEqual(tag.get('isInTransit'), false);

		});

		strictEqual(tag.get('isDeleting'), true);
		strictEqual(tag.get('isDeleted'), false);
		strictEqual(tag.get('isSaving'), false);
		strictEqual(tag.get('isReloading'), false);
		strictEqual(tag.get('isLoaded'), true);
		strictEqual(tag.get('isDirty'), false);
		strictEqual(tag.get('isCreating'), false);
		strictEqual(tag.get('isNew'), false);
		strictEqual(tag.get('isInTransit'), true);
		stop();
	});

	test('Resetting changes', function() {
		expect(3);

		var tag = store.getRecord('tag', '1');

		strictEqual(tag.get('isDirty'), false);
		tag.set('name', 'foobar');
		strictEqual(tag.get('isDirty'), true);
		tag.rollbackAttributes();
		strictEqual(tag.get('isDirty'), false);
	});
})();



<|start_filename|>tasks/register_handlebars_helpers.js<|end_filename|>
'use strict';

var Handlebars = require('handlebars');

String.prototype.startsWith = String.prototype.startsWith || function(prefix) {
	return this.indexOf(prefix) === 0;
};

String.prototype.endsWith = String.prototype.endsWith || function(suffix) {
	return this.indexOf(suffix, this.length - suffix.length) >= 0;
};

module.exports = function(grunt) {
	grunt.registerTask('register_handlebars_helpers', function() {
		Handlebars.registerHelper('link-to', linkTo);
		Handlebars.registerHelper('link-to-class', linkToClass);
		Handlebars.registerHelper('link-to-method', linkToMethod);
		Handlebars.registerHelper('link-to-property', linkToProperty);
		Handlebars.registerHelper('strip-outer-paragraph', stripOuterParagraph);
	});
};

function linkTo(content, href) {
	return new Handlebars.SafeString('<a href="' + href + '">' + content + '</a>');
}

function linkToClass(content, name, options) {
	if (!options) {
		options = name;
		name = content;
	}

	return new Handlebars.SafeString('<a href="/api/' + name + '.html">' + content + '</a>');
}

function linkToMethod(linkName, className, methodName, options) {
	return linkToClassItem(linkName, className, methodName, options, 'method');
}

function linkToProperty(linkName, className, propertyName, options) {
	return linkToClassItem(linkName, className, propertyName, options, 'property');
}

function linkToClassItem(linkName, className, itemName, options, type) {
	if (!options) {
		options = itemName;
		itemName = className;
		className = linkName;
		linkName = itemName;
	}

	var html = '<a href="/api/' + className + '.html#' + type + '_' + itemName + '">' + linkName + '</a>';

	return new Handlebars.SafeString(html);
}

function stripOuterParagraph(text) {
	text = text.trim();

	if (text.startsWith('<p>') && text.endsWith('</p>')) {
		text = text.substring(3, text.length - 4);
	}

	return new Handlebars.SafeString(text);
}

<|start_filename|>src/model/relationship_load.js<|end_filename|>
import Ember from 'ember';

import { values as objectValues } from 'ember-graph/util/util';
import {
	RelationshipTypes,
	RelationshipStates
} from 'ember-graph/constants';


var HAS_ONE_KEY = RelationshipTypes.HAS_ONE_KEY;
var HAS_MANY_KEY = RelationshipTypes.HAS_MANY_KEY;

var CLIENT_STATE = RelationshipStates.CLIENT_STATE;
var SERVER_STATE = RelationshipStates.SERVER_STATE;
var DELETED_STATE = RelationshipStates.DELETED_STATE;

// TODO: This can probably be moved into the store to be more model-agnostic
// Idea: load attributes into records directly, but load relationships into store
// Split the data apart in `pushPayload`
export default {

	/**
	 * Sets up relationships given to the constructor for this record.
	 * Equivalent to calling the relationship functions individually.
	 *
	 * @private
	 */
	initializeRelationships: function(json) {
		this.constructor.eachRelationship(function(name, meta) {
			var value = json[name];

			if (value === undefined) {
				return;
			}

			this.set('initializedRelationships.' + name, true);

			if (meta.kind === HAS_MANY_KEY) {
				value.forEach(function(v) {
					switch (Ember.typeOf(v)) {
						case 'string':
							this.addToRelationship(name, v);
							break;
						case 'instance':
							this.addToRelationship(name, v.get('id'), v.get('typeKey'));
							break;
						default:
							this.addToRelationship(name, v.id, v.type);
							break;
					}
				}, this);
			} else {
				switch (Ember.typeOf(value)) {
					case 'string':
						this.setHasOneRelationship(name, value);
						break;
					case 'null':
						// It's already null
						break;
					case 'instance':
						this.setHasOneRelationship(name, value.get('id'), value.get('typeKey'));
						break;
					default:
						this.setHasOneRelationship(name, value.id, value.type);
						break;
				}
			}
		}, this);
	},

	/**
	 * Merges relationship data from the server into the relationships
	 * already connected to this record. Any absolutely correct choices
	 * are made automatically, while choices that come down to preference
	 * are decided based on the configurable store properties.
	 *
	 * @param {Object} json
	 * @private
	 */
	loadRelationshipsFromServer: function(json) {
		this.constructor.eachRelationship(function(name, meta) {
			var otherKind = null;

			if (meta.inverse) {
				otherKind = this.get('store').modelFor(meta.relatedType).metaForRelationship(meta.inverse).kind;
			}

			// TODO: I don't much like this here. Same for the attributes one.
			if (meta.isRequired && json[name] === undefined) {
				throw new Ember.Error('Your JSON is missing the required `' + name + '` relationship.');
			}

			if (json[name] === undefined) {
				json[name] = meta.getDefaultValue();
			}

			if (meta.kind === HAS_MANY_KEY) {
				switch (otherKind) {
					case HAS_ONE_KEY:
						this.connectHasManyToHasOne(name, meta, json[name]);
						break;
					case HAS_MANY_KEY:
						this.connectHasManyToHasMany(name, meta, json[name]);
						break;
					default:
						this.connectHasManyToNull(name, meta, json[name]);
						break;
				}
			} else {
				if (json[name]) {
					switch (otherKind) {
						case HAS_ONE_KEY:
							this.connectHasOneToHasOne(name, meta, json[name]);
							break;
						case HAS_MANY_KEY:
							this.connectHasOneToHasMany(name, meta, json[name]);
							break;
						default:
							this.connectHasOneToNull(name, meta, json[name]);
							break;
					}
				} else {
					switch (otherKind) {
						case HAS_ONE_KEY:
							this.disconnectHasOneFromHasOne(name, meta);
							break;
						case HAS_MANY_KEY:
							this.disconnectHasOneFromHasMany(name, meta);
							break;
						default:
							this.disconnectHasOneFromNull(name, meta);
							break;
					}
				}
			}
		}, this);
	},

	disconnectHasOneFromNull() {
		return this.disconnectHasOneFromHasMany(...arguments);
	},

	disconnectHasOneFromHasOne() {
		return this.disconnectHasOneFromHasMany(...arguments);
	},

	disconnectHasOneFromHasMany: function(name, meta) {
		var store = this.get('store');
		var relationships = store.sortHasOneRelationships(this.typeKey, this.get('id'), name);

		if (relationships[DELETED_STATE].length > 0) {
			relationships[DELETED_STATE].forEach(function(relationship) {
				store.deleteRelationship(relationship);
			}, this);
		}

		if (!relationships[SERVER_STATE] && !relationships[CLIENT_STATE]) {
			return;
		}

		if (relationships[SERVER_STATE] && !relationships[CLIENT_STATE]) {
			store.deleteRelationship(relationships[SERVER_STATE]);
			return;
		}

		if (!relationships[SERVER_STATE] && relationships[CLIENT_STATE]) {
			if (!store.get('sideWithClientOnConflict')) {
				store.deleteRelationship(relationships[CLIENT_STATE]);
			}
		}
	},

	connectHasOneToNull() {
		return this.connectHasOneToHasMany(...arguments);
	},

	connectHasOneToHasOne: function(name, meta, value) {
		// TODO: This is going to be LONG. But make it right, then make it good
		var thisType = this.typeKey;
		var thisId = this.get('id');
		var store = this.get('store');
		var sideWithClientOnConflict = store.get('sideWithClientOnConflict');

		var theseValues = store.sortHasOneRelationships(thisType, thisId, name);
		var otherValues = store.sortHasOneRelationships(value.type, value.id, meta.inverse);

		var thisCurrent = theseValues[SERVER_STATE] || theseValues[CLIENT_STATE] || null;
		var otherCurrent = otherValues[SERVER_STATE] || otherValues[CLIENT_STATE] || null;
		if (thisCurrent === otherCurrent) {
			store.changeRelationshipState(thisCurrent, SERVER_STATE);
			return;
		}

		// Hehe, I'm going to look back on this one day...
		var handled;

		if (!theseValues[SERVER_STATE] && !theseValues[CLIENT_STATE] && theseValues[DELETED_STATE].length <= 0) {
			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.deleteRelationship(otherValues[SERVER_STATE]);
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}
				return;
			}

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = false;

				otherValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = true;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				if (!handled) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = false;

				otherValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.deleteRelationship(otherValues[CLIENT_STATE]);
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = true;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				if (!handled) {
					if (sideWithClientOnConflict) {
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, DELETED_STATE);
					} else {
						store.deleteRelationship(otherValues[CLIENT_STATE]);
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, SERVER_STATE);
					}
				}

				return;
			}
		}

		if (theseValues[SERVER_STATE] && !theseValues[CLIENT_STATE] && theseValues[DELETED_STATE].length <= 0) {
			store.deleteRelationship(theseValues[SERVER_STATE]);

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.deleteRelationship(otherValues[SERVER_STATE]);
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = false;

				otherValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = true;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				if (!handled) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = false;

				otherValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.deleteRelationship(otherValues[CLIENT_STATE]);
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = true;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				if (!handled) {
					if (sideWithClientOnConflict) {
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, DELETED_STATE);
					} else {
						store.deleteRelationship(otherValues[CLIENT_STATE]);
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, SERVER_STATE);
					}
				}

				return;
			}
		}

		if (!theseValues[SERVER_STATE] && theseValues[CLIENT_STATE] && theseValues[DELETED_STATE].length <= 0) {
			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.deleteRelationship(otherValues[SERVER_STATE]);

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				otherValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				otherValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}
		}

		if (!theseValues[SERVER_STATE] && !theseValues[CLIENT_STATE] && theseValues[DELETED_STATE].length > 0) {
			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				theseValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				theseValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				store.deleteRelationship(otherValues[SERVER_STATE]);
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				theseValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = null;

				theseValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = relationship;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				theseValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship !== handled) {
						store.deleteRelationship(relationship);
					}
				});

				if (handled === null) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				handled = null;

				theseValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.deleteRelationship(otherValues[CLIENT_STATE]);
							store.changeRelationshipState(relationship, SERVER_STATE);
						}

						handled = relationship;
					} else {
						store.deleteRelationship(relationship);
					}
				}, this);

				theseValues[DELETED_STATE].forEach(function(relationship) {
					if (relationship !== handled) {
						store.deleteRelationship(relationship);
					}
				});

				if (handled === null) {
					if (sideWithClientOnConflict) {
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, DELETED_STATE);
					} else {
						store.deleteRelationship(otherValues[CLIENT_STATE]);
						store.createRelationship(thisType, thisId, name,
							value.type, value.id, meta.inverse, SERVER_STATE);
					}
				}

				return;
			}
		}

		if (!theseValues[SERVER_STATE] && theseValues[CLIENT_STATE] && theseValues[DELETED_STATE].length > 0) {
			theseValues[DELETED_STATE].forEach(function(relationship) {
				store.deleteRelationship(relationship);
			});

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				store.deleteRelationship(otherValues[SERVER_STATE]);

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && !otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				otherValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}

			if (!otherValues[SERVER_STATE] && otherValues[CLIENT_STATE] && otherValues[DELETED_STATE].length > 0) {
				otherValues[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(theseValues[CLIENT_STATE]);
					store.deleteRelationship(otherValues[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}

				return;
			}
		}
	},

	connectHasOneToHasMany: function(name, meta, value) {
		var thisType = this.typeKey;
		var thisId = this.get('id');
		var store = this.get('store');
		var relationships = store.sortHasOneRelationships(thisType, thisId, name);
		var sideWithClientOnConflict = store.get('sideWithClientOnConflict');

		// TODO: Make it right, then make it good
		if (relationships[SERVER_STATE] && relationships[SERVER_STATE].otherType(this) === value.type &&
			relationships[SERVER_STATE].otherId(this) === value.id) {
			return;
		}

		if (relationships[CLIENT_STATE] && relationships[CLIENT_STATE].otherType(this) === value.type &&
			relationships[CLIENT_STATE].otherId(this) === value.id) {
			store.changeRelationshipState(relationships[CLIENT_STATE], SERVER_STATE);

			if (relationships[DELETED_STATE].length > 0) {
				relationships[DELETED_STATE].forEach(store.deleteRelationship.bind(store));
			}

			return;
		}

		if (!relationships[SERVER_STATE] && !relationships[CLIENT_STATE] && relationships[DELETED_STATE].length <= 0) {
			store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
			return;
		}

		if (relationships[SERVER_STATE] && !relationships[CLIENT_STATE] && relationships[DELETED_STATE].length <= 0) {
			store.deleteRelationship(relationships[SERVER_STATE]);
			store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
			return;
		}

		if (!relationships[SERVER_STATE] && relationships[CLIENT_STATE] && relationships[DELETED_STATE].length <= 0) {
			if (sideWithClientOnConflict) {
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
			} else {
				store.deleteRelationship(relationships[CLIENT_STATE]);
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
			}

			return;
		}

		var handled;

		if (!relationships[SERVER_STATE] && !relationships[CLIENT_STATE] && relationships[DELETED_STATE].length >= 0) {
			handled = false;

			relationships[DELETED_STATE].forEach(function(relationship) {
				if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
					if (sideWithClientOnConflict) {
						// NOOP
					} else {
						store.changeRelationshipState(relationship, SERVER_STATE);
					}

					handled = true;
				} else {
					store.deleteRelationship(relationship);
				}
			}, this);

			if (!handled) {
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
			}

			return;
		}

		if (!relationships[SERVER_STATE] && relationships[CLIENT_STATE] && relationships[DELETED_STATE].length >= 0) {
			handled = false;

			relationships[DELETED_STATE].forEach(function(relationship) {
				if (relationship.otherType(this) === value.type && relationship.otherId(this) === value.id) {
					if (sideWithClientOnConflict) {
						// NOOP
					} else {
						store.deleteRelationship(relationships[CLIENT_STATE]);
						store.changeRelationshipState(relationship, SERVER_STATE);
					}

					handled = true;
				} else {
					store.deleteRelationship(relationship);
				}
			}, this);

			if (!handled) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(relationships[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}
			}

			return;
		}
	},

	connectHasManyToNull() {
		return this.connectHasManyToHasMany(...arguments);
	},

	connectHasManyToHasOne: function(name, meta, values) {
		var thisType = this.typeKey;
		var thisId = this.get('id');
		var store = this.get('store');
		var sideWithClientOnConflict = store.get('sideWithClientOnConflict');

		var valueMap = values.reduce(function(map, value) {
			map[value.type + ':' + value.id] = value;
			return map;
		}, {});

		var relationships = store.relationshipsForRecord(thisType, thisId, name);

		relationships.forEach(function(relationship) {
			var valueKey = relationship.otherType(this) + ':' + relationship.otherId(this);

			if (valueMap[valueKey]) {
				switch (relationship.get('state')) {
					case SERVER_STATE:
						// NOOP
						break;
					case DELETED_STATE:
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.changeRelationshipState(relationship, SERVER_STATE);
						}
						break;
					case CLIENT_STATE:
						store.changeRelationshipState(relationship, SERVER_STATE);
						break;
				}
			} else {
				switch (relationship.get('state')) {
					case SERVER_STATE:
					case DELETED_STATE:
						store.deleteRelationship(relationship);
						break;
					case CLIENT_STATE:
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.deleteRelationship(relationship);
						}
						break;
				}
			}

			delete valueMap[valueKey];
		}, this);

		// We've narrowed it down to relationships that have to be created from scratch. (Possibly with conflicts.)
		objectValues(valueMap, function(key, value) {
			var conflicts = store.sortHasOneRelationships(value.type, value.id, meta.inverse);

			if (!conflicts[SERVER_STATE] && !conflicts[CLIENT_STATE] && conflicts[DELETED_STATE].length <= 0) {
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (conflicts[SERVER_STATE] && !conflicts[CLIENT_STATE] && conflicts[DELETED_STATE].length <= 0) {
				store.deleteRelationship(conflicts[SERVER_STATE]);
				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (!conflicts[SERVER_STATE] && conflicts[CLIENT_STATE] && conflicts[DELETED_STATE].length <= 0) {
				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(conflicts[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}
				return;
			}

			if (!conflicts[SERVER_STATE] && !conflicts[CLIENT_STATE] && conflicts[DELETED_STATE].length > 0) {
				conflicts[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				return;
			}

			if (!conflicts[SERVER_STATE] && conflicts[CLIENT_STATE] && conflicts[DELETED_STATE].length > 0) {
				conflicts[DELETED_STATE].forEach(function(relationship) {
					store.deleteRelationship(relationship);
				});

				if (sideWithClientOnConflict) {
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, DELETED_STATE);
				} else {
					store.deleteRelationship(conflicts[CLIENT_STATE]);
					store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
				}
				return;
			}
		}, this);
	},

	connectHasManyToHasMany: function(name, meta, values) {
		var thisType = this.typeKey;
		var thisId = this.get('id');
		var store = this.get('store');
		var sideWithClientOnConflict = store.get('sideWithClientOnConflict');

		var valueMap = values.reduce(function(map, value) {
			map[value.type + ':' + value.id] = value;
			return map;
		}, {});

		var relationships = store.relationshipsForRecord(thisType, thisId, name);

		relationships.forEach(function(relationship) {
			var valueKey = relationship.otherType(this) + ':' + relationship.otherId(this);

			if (valueMap[valueKey]) {
				switch (relationship.get('state')) {
					case SERVER_STATE:
						// NOOP
						break;
					case DELETED_STATE:
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.changeRelationshipState(relationship, SERVER_STATE);
						}
						break;
					case CLIENT_STATE:
						store.changeRelationshipState(relationship, SERVER_STATE);
						break;
				}
			} else {
				switch (relationship.get('state')) {
					case SERVER_STATE:
					case DELETED_STATE:
						store.deleteRelationship(relationship);
						break;
					case CLIENT_STATE:
						if (sideWithClientOnConflict) {
							// NOOP
						} else {
							store.deleteRelationship(relationship);
						}
						break;
				}
			}

			delete valueMap[valueKey];
		}, this);

		// We've narrowed it down to relationships that have to be created from scratch.
		objectValues(valueMap, function(key, value) {
			store.createRelationship(thisType, thisId, name, value.type, value.id, meta.inverse, SERVER_STATE);
		});
	}
};


<|start_filename|>src/util/string.js<|end_filename|>
import Ember from 'ember';

function startsWith(string, prefix) {
	return string.indexOf(prefix) === 0;
}

function endsWith(string, suffix) {
	return string.indexOf(suffix, string.length - suffix.length) >= 0;
}

function capitalize(string) {
	return string[0].toLocaleUpperCase() + string.substring(1);
}

function decapitalize(string) {
	return string[0].toLocaleLowerCase() + string.substring(1);
}

if (Ember.ENV.EXTEND_PROTOTYPES === true || Ember.ENV.EXTEND_PROTOTYPES.String) {

	/**
	 * Polyfill for String.prototype.startsWith
	 *
	 * @method startsWith
	 * @param {String} prefix
	 * @return {Boolean}
	 * @namespace String
	 */
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function(prefix) {
      return startsWith(this, prefix);
    };
  }

	/**
	 *Polyfill for String.prototype.endsWith
	 *
	 * @method endsWith
	 * @param {String} suffix
	 * @return {Boolean}
	 * @namespace String
	 */
  if (!String.prototype.endsWith) {
	  String.prototype.endsWith = function(suffix) {
		  return endsWith(this, suffix);
	  };
  }

	/**
	 * Capitalizes the first letter of a string.
	 *
	 * @method capitalize
	 * @return {String}
	 * @namespace String
	 */
  if (!String.prototype.capitalize) {
    String.prototype.capitalize = function() {
      return capitalize(this);
    };
  }

	/**
	 * Decapitalizes the first letter of a string.
	 *
	 * @method decapitalize
	 * @return {String}
	 * @namespace String
	 */
  if (!String.prototype.decapitalize) {
    String.prototype.decapitalize = function() {
      return decapitalize(this);
    };
  }
}

export {
	startsWith,
	endsWith,
	capitalize,
	decapitalize
};


<|start_filename|>src/serializer/json.js<|end_filename|>
import Ember from 'ember';
import Model from 'ember-graph/model/model';
import Serializer from 'ember-graph/serializer/serializer';
import EmberGraphSet from 'ember-graph/util/set';
import copy from 'ember-graph/util/copy';

import { mapBy } from 'ember-graph/util/array';
import { pluralize, singularize } from 'ember-graph/util/inflector';


/**
 * This serializer was designed to be compatible with the
 * {{link-to 'JSON API' 'http://jsonapi.org'}}
 * (the ID format, not the URL format).
 *
 * @class JSONSerializer
 * @extends Serializer
 * @constructor
 */
export default Serializer.extend({

	/**
	 * This property can be overridden if you're using polymorphic relationships
	 * in your models. Instead of strings for IDs, the serializer will use objects
	 * for IDs. Each object will contain a `type` and `id` property.
	 *
	 * @property polymorphicRelationships
	 * @type Boolean
	 * @default false
	 * @private
	 */
	polymorphicRelationships: false,

	serialize: function(record, options) {
		switch (options.requestType) {
			case 'updateRecord':
				return this.serializeDelta(record, options);
			case 'createRecord':
				var json = {};
				json[pluralize(record.typeKey)] = [this.serializeRecord(record, options)];
				return json;
			default:
				throw new Ember.Error('Invalid request type for JSON serializer.');
		}
	},

	/**
	 * Converts a single record to its JSON representation.
	 *
	 * @method serializeRecord
	 * @param {Model} record
	 * @param {Object} options
	 * @return {JSON} The JSON representation of the record
	 */
	serializeRecord: function(record, options) {
		var json = {
			links: {}
		};

		record.constructor.eachAttribute(function(name, meta) {
			var serialized = this.serializeAttribute(record, name, options);
			if (serialized) {
				json[serialized.name] = serialized.value;
			}
		}, this);

		record.constructor.eachRelationship(function(name, meta) {
			var serialized = this.serializeRelationship(record, name, options);
			if (serialized) {
				json.links[serialized.name] = serialized.value;
			}
		}, this);

		return json;
	},

	/**
	 * Serializes a single attribute for a record. This function
	 * determines how the value is serialized and what the
	 * serialized name will be. To remove the attribute
	 * from serialization, return `null` from this function. To
	 * keep the attribute, return an object like the one below:
	 *
	 * ```js
	 * {
	 *     name: "serialized_name",
	 *     value: "serialized_value"
	 * }
	 * ```
	 *
	 * By default, this function will keep the name of the
	 * attribute and serialize the value using the corresponding
	 * {{#link-to-class 'AttributeType'}}AttributeType{{/link-to-class}}.
	 *
	 * @method serializeAttribute
	 * @param {Model} record
	 * @param {String} name The name of the attribute to serialize
	 * @param {Object} options
	 * @return {Object}
	 * @protected
	 */
	serializeAttribute: function(record, name, options) {
		var meta = record.constructor.metaForAttribute(name);
		var type = this.get('store').attributeTypeFor(meta.type);

		if (meta.isServerOnly && options.requestType === 'createRecord') {
			return null;
		}

		return { name: name, value: type.serialize(record.get(name)) };
	},

	/**
	 * Serializes a single relationship for a record. This function
	 * determines how the value is serialized and what the
	 * serialized name will be. To remove the relationship
	 * from serialization, return `null` from this function. To
	 * keep the relationship, return an object like the one below:
	 *
	 * ```js
	 * {
	 *     name: "serialized_name",
	 *     value: "serialized_value"
	 * }
	 * ```
	 *
	 * By default, this function will keep the name of the
	 * relationship. For hasOne relationships, it will
	 * use either a single string ID or `null`. For hasMany
	 * relationships, it will use an array of string IDs.
	 *
	 * @method serializeRelationship
	 * @param {Model} record
	 * @param {String} name The name of the relationship to serialize
	 * @param {Object} options
	 * @return {Object}
	 * @protected
	 */
	serializeRelationship: function(record, name, options) {
		var meta = record.constructor.metaForRelationship(name);
		var value = record.get('_' + name);

		if (meta.isServerOnly && options.requestType === 'createRecord') {
			return null;
		}

		if (meta.kind === Model.HAS_ONE_KEY) {
			if (value === null || Model.isTemporaryId(value.id)) {
				return { name: name, value: null };
			}

			return {
				name: name,
				value: (this.get('polymorphicRelationships') ? value : value.id)
			};
		} else {
			value = value.filter(function(v) {
				return !Model.isTemporaryId(v.id);
			});

			return {
				name: name,
				value: (this.get('polymorphicRelationships') ? value : mapBy.call(value, 'id'))
			};
		}
	},

	/**
	 * Serializes a record's changes to a list of change operations
	 * that can be used in a JSON API `PATCH` request. The format
	 * follows the specification except for one minor detail. At the
	 * time of writing this, the `path` in a change operation must
	 * be fully qualified, but there is a change upcoming to fix
	 * that. This uses the soon-to-be format. So instead of this:
	 *
	 * ```json
	 * PATCH /photos/1
	 *
	 * [
	 *     { "op": "remove", "path": "/photos/1/links/comments/5" }
	 * ]
	 * ```
	 *
	 * It uses this:
	 *
	 * ```json
	 * PATCH /photos/1
	 *
	 * [
	 *     { "op": "remove", "path": "/links/comments/5" }
	 * ]
	 * ```
	 *
	 * Everything else remains the same. It will use the `replace`
	 * operation for attributes and hasOne relationships, and the
	 * `add` and `remove` operations for hasMany relationships.
	 *
	 * @method serializeDelta
	 * @param {Model} record
	 * @param {Object} options
	 * @return {JSON} Array of change operations
	 */
	serializeDelta: function(record, options) {
		var operations = this.serializeAttributeDelta(record, options);
		return operations.concat(this.serializeRelationshipDelta(record, options));
	},

	/**
	 * Serializes a record's attributes changes to operation objects.
	 *
	 * @method serializeAttributeDelta
	 * @param {Model} record
	 * @param {Object} options
	 * @return {JSON} Array of change operations
	 * @protected
	 */
	serializeAttributeDelta: function(record, options) {
		var changes = record.changedAttributes();
		var store = this.get('store');

		return Object.keys(changes).map(function(attributeName) {
			var meta = record.constructor.metaForAttribute(attributeName);
			var type = store.attributeTypeFor(meta.type);
			var value = type.serialize(changes[attributeName][1]);

			return { op: 'replace', path: '/' + attributeName, value: value };
		});
	},

	/**
	 * Serializes a record's relationship changes to operation objects.
	 *
	 * @method serializeRelationshipDelta
	 * @param {Model} record
	 * @param {Object} options
	 * @return {JSON} Array of change operations
	 * @protected
	 */
	serializeRelationshipDelta: function(record, options) {
		var operations = [];
		var changes = record.changedRelationships();
		var polymorphicRelationships = this.get('polymorphicRelationships');

		Object.keys(changes).forEach(function(relationshipName) {
			var values = changes[relationshipName];
			var meta = record.constructor.metaForRelationship(relationshipName);

			if (meta.kind === Model.HAS_ONE_KEY) {
				operations.push({
					op: 'replace',
					path: '/links/' + relationshipName,
					value: (polymorphicRelationships ? values[1] : (values[1] === null ? null : values[1].id))
				});
			} else if (meta.kind === Model.HAS_MANY_KEY) {
				var originalSet = EmberGraphSet.create();
				originalSet.addObjects(values[0].map(function(value) {
					return value.type + ':' + value.id;
				}));

				var currentSet = EmberGraphSet.create();
				currentSet.addObjects(values[1].map(function(value) {
					return value.type + ':' + value.id;
				}));

				values[1].forEach(function(value) {
					if (!originalSet.contains(value.type + ':' + value.id) && !Model.isTemporaryId(value.id)) {
						operations.push({
							op: 'add',
							path: '/links/' + relationshipName,
							value: (polymorphicRelationships ? value : value.id)
						});
					}
				});

				values[0].forEach(function(value) {
					if (!currentSet.contains(value.type + ':' + value.id)  && !Model.isTemporaryId(value.id)) {
						operations.push({
							op: 'remove',
							path: '/links/' + relationshipName + '/' + value.id,
							value: (polymorphicRelationships ? value : value.id)
						});
					}
				});
			}
		});

		return operations;
	},

	deserialize: function(payload, options) {
		var store = this.get('store');
		var normalized = this.transformPayload(payload || {}, options || {});

		Object.keys(normalized).forEach(function(typeKey) {
			if (typeKey === 'meta') {
				return;
			}

			var model = store.modelFor(typeKey);

			normalized[typeKey] = normalized[typeKey].map(function(json) {
				return this.deserializeRecord(model, json, options);
			}, this);
		}, this);

		return normalized;
	},

	/**
	 * Converts a payload partially to normalized JSON.
	 * The layout is the same, but the individual records
	 * themselves have yet to be deserialized.
	 *
	 * @method transformPayload
	 * @param {JSON} payload
	 * @param {Object} options
	 * @return {Object} Normalized JSON payload
	 * @protected
	 */
	transformPayload: function(payload, options) {
		if (!payload || Object.keys(payload).length === 0) {
			return {};
		}

		payload = copy(payload, true);

		var normalized = {
			meta: {
				serverMeta: payload.meta || {}
			}
		};

		delete payload.meta;

		// TODO: Query multiple types
		if (options.requestType === 'findQuery') {
			normalized.meta.matchedRecords =
				payload[pluralize(options.recordType)].map(function(record) {
					return { type: options.recordType, id: record.id + '' };
				});
		} else if (options.requestType === 'createRecord') {
			normalized.meta.createdRecord = {
				type: options.recordType,
				id: payload[pluralize(options.recordType)][0].id + ''
			};
		}

		Object.keys(payload).forEach(function(key) {
			if (key !== 'linked' && key !== 'meta') {
				normalized[singularize(key)] = payload[key];
				delete payload[key];
			}
		});

		Object.keys(payload.linked || {}).forEach(function(key) {
			var singular = singularize(key);
			normalized[singular] = (normalized[singular] || []).concat(payload.linked[key] || []);
		});

		return normalized;
	},

	/**
	 * Converts a single record from its JSON representation
	 * to the Javascript representation that the store expects.
	 *
	 * @method deserializeRecord
	 * @param {Model} model
	 * @param {JSON} json
	 * @param {Object} options
	 * @return {Object} Deserialized record
	 * @protected
	 */
	deserializeRecord: function(model, json, options) {
		if (Ember.typeOf(json.id) !== 'string' && Ember.typeOf(json.id) !== 'number') {
			throw new Ember.Error('Your JSON has an invalid ID: ' + JSON.stringify(json));
		}

		var record = {
			id: json.id + ''
		};

		model.eachAttribute(function(name, meta) {
			var deserialized = this.deserializeAttribute(model, json, name, options);
			if (deserialized) {
				record[deserialized.name] = deserialized.value;
			}
		}, this);

		json.links = json.links || {};

		model.eachRelationship(function(name, meta) {
			var deserialized = this.deserializeRelationship(model, json, name, options);
			if (deserialized) {
				record[deserialized.name] = deserialized.value;
			}
		}, this);

		return record;
	},

	/**
	 * Deserializes a single attribute for a record. This function
	 * determines how the value is deserialized and what the
	 * deserialized name will be. To remove the attribute
	 * from deserialization, return `null` from this function. To
	 * keep the attribute, return an object like the one below:
	 *
	 * ```js
	 * {
	 *     name: "deserialized_name",
	 *     value: "deserialized_value"
	 * }
	 * ```
	 *
	 * By default, this function keeps the original name and
	 * serializes the value using the corresponding
	 * {{#link-to-class 'AttributeType'}}AttributeType{{/link-to-class}}.
	 * If the value is missing, it attempts to use the default
	 * value. If it's missing and required, it has to throw an
	 * error.
	 *
	 * @method deserializeAttribute
	 * @param {Class} model
	 * @param {JSON} json
	 * @param {String} name
	 * @param {Object} options
	 * @return {Object}
	 * @protected
	 */
	deserializeAttribute: function(model, json, name, options) {
		var meta = model.metaForAttribute(name);
		var type = this.get('store').attributeTypeFor(meta.type);
		var value = json[name];

		if (value === undefined) {
			if (meta.isRequired) {
				var error = { id: json.id, typeKey: model.typeKey, name: name };
				throw new Ember.Error('Attribute was missing: ' + JSON.stringify(error));
			}

			return {
				name: name,
				value: (meta.getDefaultValue() === undefined ? type.get('defaultValue') : meta.getDefaultValue())
			};
		} else {
			return { name: name, value: type.deserialize(value) };
		}
	},

	/**
	 * Deserializes a single relationship for a record. This function
	 * determines how the value is deserialized and what the
	 * deserialized name will be. To remove the relationship
	 * from deserialization, return `null` from this function. To
	 * keep the relationship, return an object like the one below:
	 *
	 * ```js
	 * {
	 *     name: "deserialized_name",
	 *     value: "deserialized_value"
	 * }
	 * ```
	 *
	 * By default, this function keeps the original name. HasOne
	 * relationships are expected to be either `null`, or a number
	 * or string. Numbers are converted to strings for the store.
	 * HasMany relationships are expected to be an array of
	 * numbers or strings. If any relationship is missing or invalid,
	 * the default value will be used. If it's missing or invalid
	 * and required, an error will be thrown.
	 *
	 * @method deserializeRelationship
	 * @param {Class} model
	 * @param {JSON} json
	 * @param {String} name
	 * @param {Object} options
	 * @return {Object}
	 * @protected
	 */
	deserializeRelationship: function(model, json, name, options) {
		var meta = model.metaForRelationship(name);
		var value = json.links[name];

		if (value === undefined) {
			if (meta.isRequired) {
				throw new Ember.Error('Missing `' + name + '` relationship: ' + JSON.stringify(json));
			}

			return { name: name, value: meta.getDefaultValue() };
		} else {
			if (meta.kind === Model.HAS_MANY_KEY) {
				return this.deserializeHasManyRelationship(model, name, value);
			} else {
				return this.deserializeHasOneRelationship(model, name, value);
			}
		}
	},

	/**
	 * After {{link-to-method 'JSONSerializer' 'deserializeRelationship'}} has checked
	 * for missing values, it delegates to this function to deserialize a single
	 * hasOne relationship. Their return types are the same.
	 *
	 * @method deserializeHasOneRelationship
	 * @param {Class} model
	 * @param {String} name
	 * @param {Object|String|Number} value
	 * @param {Object} options
	 * @returns {Object}
	 * @protected
	 */
	deserializeHasOneRelationship: function(model, name, value, options) {
		if (value === null) {
			return { name: name, value: null };
		}

		var polymorphic = this.get('polymorphicRelationships');

		return {
			name: name,
			value: {
				type: (polymorphic ? value.type : model.metaForRelationship(name).relatedType),
				id: (polymorphic ? value.id : value) + ''
			}
		};
	},

	/**
	 * After {{link-to-method 'JSONSerializer' 'deserializeRelationship'}} has checked
	 * for missing values, it delegates to this function to deserialize a single
	 * hasMany relationship. Their return types are the same.
	 *
	 * @method deserializeHasManyRelationship
	 * @param {Class} model
	 * @param {String} name
	 * @param {Object[]|String[]|Number[]} values
	 * @param {Object} options
	 * @returns {Object}
	 * @protected
	 */
	deserializeHasManyRelationship: function(model, name, values, options) {
		var relatedType = model.metaForRelationship(name).relatedType;
		var polymorphic = this.get('polymorphicRelationships');

		var mapped =  values.map(function(value) {
			return {
				type: (polymorphic ? value.type : relatedType),
				id: (polymorphic ? value.id : value) + ''
			};
		});

		return { name: name, value: mapped };
	}
});


<|start_filename|>tasks/options/uglify.js<|end_filename|>
/* eslint-disable camelcase */
module.exports = {
	options: {
		mangle: true,
		compress: {
			global_defs: {
				DEBUG: false,
				RELEASE: true
			},
			sequences: true,
			properties: true,
			drop_debugger: true,
			unsafe: false,
			conditionals: true,
			comparisons: true,
			evaluate: true,
			booleans: true,
			dead_code: true,
			loops: true,
			unused: true,
			hoist_funs: false,
			hoist_vars: false,
			if_return: true,
			join_vars: true,
			cascade: true,
			warnings: true,
			negate_iife: false,
			pure_getters: false,
			pure_funcs: null,
			drop_console: true
		},
		screw_ie8: true
	},

	release: {
		files: {
			'dist/ember-graph.min.js': 'dist/ember-graph.prod.js'
		}
	}
};
/* eslint-enable camelcase */

<|start_filename|>src/attribute_type/date.js<|end_filename|>
import Ember from 'ember';
import AttributeType from 'ember-graph/attribute_type/type';

/**
 * @class DateType
 * @extends AttributeType
 * @constructor
 */
export default AttributeType.extend({

	/**
	 * Converts any Date object, number or string to a timestamp.
	 *
	 * @method serialize
	 * @param {Date|Number|String} date
	 * @return {Number}
	 */
	serialize: function(date) {
		switch (Ember.typeOf(date)) {
			case 'date':
				return date.getTime();
			case 'number':
				return date;
			case 'string':
				return new Date(date).getTime();
			default:
				return null;
		}
	},

	/**
	 * Converts any numeric or string timestamp to a Date object.
	 * Everything else gets converted to `null`.
	 *
	 * @method deserialize
	 * @param {Number|String} timestamp
	 * @return {Date}
	 */
	deserialize: function(timestamp) {
		switch (Ember.typeOf(timestamp)) {
			case 'number':
			case 'string':
				return new Date(timestamp);
			default:
				return null;
		}
	},

	/**
	 * Converts both arguments to a timestamp, then compares.
	 *
	 * @param {Date} a
	 * @param {Date} b
	 * @return {Boolean}
	 */
	isEqual: function(a, b) {
		var aNone = (a === null);
		var bNone = (b === null);

		if (aNone && bNone) {
			return true;
		} else if ((aNone && !bNone) || (!aNone && bNone)) {
			return false;
		} else {
			return (new Date(a).getTime() === new Date(b).getTime());
		}
	}
});

<|start_filename|>src/adapter/memory.js<|end_filename|>
import Ember from 'ember';
import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter';

const Promise = Ember.RSVP.Promise;

/**
 * This adapter stores all of your changes in memory, mainly for testing
 * purposes. To initialize the memory with an initial data set, override
 * the {{link-to-method 'MemoryAdapter' 'getInitialPayload'}} hook to
 * return the data that you want to load into memory.
 *
 * To customize the the behavior for getting or saving records, you can
 * override any of the following methods:
 * {{link-to-method 'MemoryAdapter' 'serverFindRecord'}},
 * {{link-to-method 'MemoryAdapter' 'serverFindMany'}},
 * {{link-to-method 'MemoryAdapter' 'serverFindAll'}},
 * {{link-to-method 'MemoryAdapter' 'serverCreateRecord'}},
 * {{link-to-method 'MemoryAdapter' 'serverDeleteRecord'}},
 * {{link-to-method 'MemoryAdapter' 'serverUpdateRecord'}}.
 *
 * @class MemoryAdapter
 * @extends EmberGraphAdapter
 */
export default EmberGraphAdapter.extend({

	database: null,

	getDatabase: function() {
		try {
			var database = this.get('database');

			if (database) {
				return Promise.resolve(database);
			} else {
				return Promise.resolve({ records: {}, relationships: [] });
			}
		} catch (error) {
			return Promise.reject(error);
		}
	},

	setDatabase: function(database) {
		try {
			this.set('database', database);
			return Promise.resolve(database);
		} catch (error) {
			return Promise.reject(error);
		}
	},

	shouldInitializeDatabase: function() {
		return true;
	},

	/**
	 * Initializes the database (if configured to do so).
	 * This function is called at adapter initialization
	 * (which is probably when it's looked up by the container).
	 *
	 * @method initializeDatabase
	 * @private
	 */
	initializeDatabase: function() {
		this._super();
	},

	initializeDatabaseOnInit: Ember.on('init', function() {
		this.initializeDatabase();
	})

});

<|start_filename|>src/attribute_type/type.js<|end_filename|>
import Ember from 'ember';

/**
 * Specifies the details of a custom attribute type.
 * Comes with reasonable defaults that can be used for some extended types.
 *
 * @class AttributeType
 * @constructor
 */
export default Ember.Object.extend({

	/**
	 * The default value to use if a value of this type is missing.
	 * Can be overridden in subclasses.
	 *
	 * @property defaultValue
	 * @type Any
	 * @default null
	 * @final
	 */
	defaultValue: null,

	/**
	 * Converts a value of this type to its JSON form.
	 * The default function returns the value given.
	 *
	 * @method serialize
	 * @param {Any} obj Javascript value
	 * @return {JSON} JSON representation
	 */
	serialize: function(obj) {
		return obj;
	},

	/**
	 * Converts a JSON value to its Javascript form.
	 * The default function returns the value given.
	 *
	 * @method deserialize
	 * @param {JSON} json JSON representation of object
	 * @return {Any} Javascript value
	 */
	deserialize: function(json) {
		return json;
	},

	/**
	 * Determines if two values of this type are equal.
	 * Defaults to using `===`.
	 *
	 * @method isEqual
	 * @param {Any} a Javascript object
	 * @param {Any} b Javascript object
	 * @returns {Boolean} Whether or not the objects are equal or not
	 */
	isEqual: function(a, b) {
		return (a === b);
	}
});

<|start_filename|>test/adapter/local_storage.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var adapter;
	var LOCAL_STORAGE_KEY = 'ember-graph.local_storage_test';

	module('LocalStorageAdapter Test', {
		setup: function() {
			delete localStorage[LOCAL_STORAGE_KEY];

			var DEMO_DATA = {
				user: [{
					id: '1',
					name: '<NAME>',
					posts: [{ type: 'post', id: 1 }, 2]
				}, {
					id: '2',
					name: '<NAME>',
					posts: [{ type: 'post', id: 3 }, { type: 'post', id: '4' }]
				}, {
					id: '4',
					name: 'Rando',
					favoritePost: 4
				}],

				post: [{
					id: 1,
					title: 'Post 1',
					author: { type: 'user', id: '1' }
				}, {
					id: 2,
					title: 'Post 2',
					author: { type: 'user', id: 1 }
				}, {
					id: 3,
					title: 'Post 3',
					author: { type: 'user', id: '2' }
				}, {
					id: 4,
					title: 'Post 4',
					author: 2
				}]
			};

			store = setupStore({
				user: EG.Model.extend({
					name: EG.attr({
						type: 'string'
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						defaultValue: []
					}),

					favoritePost: EG.hasOne({
						relatedType: 'post',
						inverse: null,
						defaultValue: null
					})
				}),

				post: EG.Model.extend({
					title: EG.attr({
						type: 'string'
					}),

					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts'
					})
				})
			}, {
				adapter: EG.LocalStorageAdapter.extend({
					localStorageKey: LOCAL_STORAGE_KEY,

					shouldInitializeDatabase: function() {
						return true;
					},

					getInitialPayload: function() {
						return DEMO_DATA;
					}
				})
			});

			adapter = store.adapterFor('application');
		}
	});

	test('Initializing a valid test payload works', function() {
		expect(1);

		var relationshipToString = function(r) {
			var one = r.t1 + ':' + r.i1 + ':' + r.n1;
			var two = r.t2 + ':' + r.i2 + ':' + r.n2;
			return (one < two ? one + '::' + two : two  + '::' + one);
		};

		var db = JSON.parse(localStorage[LOCAL_STORAGE_KEY]);
		db.relationships = db.relationships.map(relationshipToString);

		var expected = {
			records: {
				user: {
					'1': { name: '<NAME>' },
					'2': { name: '<NAME>' },
					'4': { name: 'Rando' }
				},
				post: {
					'1': { title: 'Post 1' },
					'2': { title: 'Post 2' },
					'3': { title: 'Post 3' },
					'4': { title: 'Post 4' }
				}
			},

			relationships: [
				{ t1: 'post', i1: '1', n1: 'author', t2: 'user', i2: '1', n2: 'posts' },
				{ t1: 'post', i1: '2', n1: 'author', t2: 'user', i2: '1', n2: 'posts' },
				{ t1: 'post', i1: '3', n1: 'author', t2: 'user', i2: '2', n2: 'posts' },
				{ t1: 'post', i1: '4', n1: 'author', t2: 'user', i2: '2', n2: 'posts' },
				{ t1: 'post', i1: '4', n1: null, t2: 'user', i2: '4', n2: 'favoritePost' }
			].map(relationshipToString)
		};

		deepEqual(db, expected);
	});

	test('Initializing an invalid test payload fails (missing record)', function() {
		expect(1);

		throws(function() {
			adapter.convertAndVerifyPayload({
				user: [{
					id: '1',
					name: '',
					posts: [
						{ type: 'post', id: 'wontexist' }
					]
				}]
			});
		});
	});

	test('Initializing an invalid test payload fails (missing required attribute)', function() {
		expect(1);

		throws(function() {
			adapter.convertAndVerifyPayload({
				user: [{
					id: '1',
					posts: [
						{ type: 'post', id: 'wontexist' }
					]
				}]
			});
		});
	});

	test('Initializing an invalid test payload fails (missing required attribute)', function() {
		expect(1);

		throws(function() {
			adapter.convertAndVerifyPayload({
				user: [{
					id: '1',
					posts: [
						{ type: 'post', id: 'wontexist' }
					]
				}]
			});
		});
	});


	test('Initializing an invalid test payload fails (too many hasOne relationships)', function() {
		expect(1);

		throws(function() {
			adapter.convertAndVerifyPayload({
				user: [{
					id: '1',
					name: '',
					posts: [
						{ type: 'post', id: '1' }
					]
				}, {
					id: '2',
					name: '',
					posts: [
						{ type: 'post', id: '1' }
					]
				}],
				post: [{
					id: '1',
					title: 'foo',
					author: { type: 'user', id: '1' }
				}]
			});
		});
	});

	asyncTest('Get an existing record', function() {
		expect(1);

		adapter.findRecord('user', '1').then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				user: [{
					id: '1',
					name: '<NAME>',
					favoritePost: null,
					posts: [
						{ type: 'post', id: '1' },
						{ type: 'post', id: '2' }
					]
				}]
			});
		}, function() {
			start();
		});
	});

	asyncTest('Get several existing records', function() {
		expect(1);

		adapter.findMany('post', ['1', '2', '4']).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				post: [{
					id: '1',
					title: 'Post 1',
					author: { type: 'user', id: '1' }
				}, {
					id: '2',
					title: 'Post 2',
					author: { type: 'user', id: '1' }
				}, {
					id: '4',
					title: 'Post 4',
					author: { type: 'user', id: '2' }
				}]
			});
		});
	});

	asyncTest('Get all records of a type', function() {
		expect(1);

		adapter.findAll('post').then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				post: [{
					id: '1',
					title: 'Post 1',
					author: { type: 'user', id: '1' }
				}, {
					id: '2',
					title: 'Post 2',
					author: { type: 'user', id: '1' }
				}, {
					id: '3',
					title: 'Post 3',
					author: { type: 'user', id: '2' }
				}, {
					id: '4',
					title: 'Post 4',
					author: { type: 'user', id: '2' }
				}]
			});
		});
	});

	asyncTest('Get a non-existing record', function() {
		expect(1);

		adapter.findRecord('user', 'wontexist').catch(function(error) {
			start();
			strictEqual(error.status, 404);
		});
	});

	asyncTest('Get many records including a non-existing one', function() {
		expect(1);

		adapter.findMany('user', ['1', '2', '3', '4']).catch(function(error) {
			start();
			strictEqual(error.status, 404);
		});
	});

	asyncTest('Query records without query implemented', function() {
		expect(1);

		adapter.findQuery('user', {}).catch(function(error) {
			start();
			ok(error.toString().match(/findQuery/g));
		});
	});

	asyncTest('Create and save a new record 1', function() {
		expect(1);

		var id;
		var user = store.createRecord('user', {
			name: '<NAME>',
			posts: []
		});

		user.save().then(function(record) {
			id = record.get('id');
			return adapter.findRecord('user', id);
		}).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				user: [{
					id: id,
					name: '<NAME>',
					favoritePost: null,
					posts: []
				}]
			});
		});
	});

	asyncTest('Create and save a new record 2', function() {
		expect(1);

		var id;
		var post = store.createRecord('post', {
			author: '1',
			title: 'Test'
		});

		post.save().then(function(record) {
			id = record.get('id');
			return adapter.findRecord('post', id);
		}).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				post: [{
					id: id,
					title: 'Test',
					author: { type: 'user', id: '1' }
				}]
			});
		});
	});

	asyncTest('Create and save a new record 3', function() {
		expect(3);

		var id;
		var user = store.createRecord('user', {
			name: '<NAME>',
			posts: ['2']
		});

		user.save().then(function(record) {
			id = record.get('id');
			return adapter.findRecord('user', id);
		}).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				user: [{
					id: id,
					name: '<NAME>',
					favoritePost: null,
					posts: [{ type: 'post', id: '2' }]
				}]
			});

			stop();

			return adapter.findRecord('post', '2');
		}).then(function(payload) {
			start();
			strictEqual(payload.post[0].author.id, id);
			stop();

			return adapter.findRecord('user', '1');
		}).then(function(payload) {
			start();
			ok(payload.user[0].posts.mapBy('id').indexOf('2') < 0);
		});
	});

	asyncTest('Update an existing record', function() {
		expect(2);

		store.find('post', '4').then(function(post) {
			post.set('title', '4444');
			post.setHasOneRelationship('author', '4');
			return post.save();
		}).then(function() {
			return adapter.findRecord('post', '4');
		}).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				post: [{
					id: '4',
					title: '4444',
					author: { type: 'user', id: '4' }
				}]
			});

			stop();

			return adapter.findRecord('user', '4');
		}).then(function(payload) {
			start();

			delete payload.meta;
			deepEqual(payload, {
				user: [{
					id: '4',
					name: 'Rando',
					favoritePost: { type: 'post', id: '4' },
					posts: [
						{ type: 'post', id: '4' }
					]
				}]
			});
		});
	});

	asyncTest('Delete existing record', function() {
		expect(1);

		store.find('post', '4').then(function(post) {
			return post.destroy();
		}).then(function() {
			start();

			adapter.findRecord('post', '4').catch(function(error) {
				strictEqual(error.status, 404);
			});
		});
	});
})();

<|start_filename|>test/attribute_type/number.js<|end_filename|>
(function() {
	'use strict';

	var type = EG.NumberType.create();

	module('Number Attribute Type Test');

	test('Serialization works correctly', function() {
		expect(7);

		strictEqual(500, type.serialize(500));
		strictEqual(5, type.serialize(new Number(5))); // eslint-disable-line no-new-wrappers
		strictEqual(0, type.serialize(null));
		strictEqual(0, type.serialize());
		strictEqual(0, type.serialize({}));
		strictEqual(4, type.serialize('4'));
		strictEqual(0, type.serialize(true));
	});

	test('Deserialization works correctly', function() {
		expect(8);

		strictEqual(type.deserialize(undefined), 0);
		strictEqual(type.deserialize(null), 0);
		strictEqual(type.deserialize(42), 42);
		strictEqual(type.deserialize(false), 0);
		strictEqual(type.deserialize(true), 0);
		strictEqual(type.deserialize('500'), 500);
		strictEqual(type.deserialize({}), 0);
		strictEqual(type.deserialize([]), 0);
	});
})();

<|start_filename|>test/model/relationship/general.js<|end_filename|>
(function() {
	'use strict';

	var store;

	module('General Relationship Functionality', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					vertices: EG.hasMany({
						relatedType: 'vertex',
						isRequired: false,
						inverse: 'owner',
						defaultValue: [{ type: 'vertex', id: '0' }]
					})
				}),

				vertex: EG.Model.extend({
					owner: EG.hasOne({
						relatedType: 'user',
						isRequired: true,
						inverse: 'vertices',
						readOnly: true
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						isRequired: false,
						inverse: null
					})
				}),

				tag: EG.Model.extend()
			});

			store.pushPayload({
				user: [
					{
						id: '1',
						vertices: [
							{ type: 'vertex', id: '1' },
							{ type: 'vertex', id: '2' },
							{ type: 'vertex', id: '3' },
							{ type: 'vertex', id: '4' }
						]
					},
					{
						id: '2'
					}
				],
				vertex: [
					{
						id: '1',
						owner: { type: 'user', id: '1' },
						tags: [
							{ type: 'tag', id: '1' },
							{ type: 'tag', id: '3' },
							{ type: 'tag', id: '5' }
						]
					},
					{
						id: '2',
						owner: { type: 'user', id: '1' },
						tags: [
							{ type: 'tag', id: '2' },
							{ type: 'tag', id: '4' },
							{ type: 'tag', id: '6' }
						]
					},
					{
						id: '4',
						owner: { type: 'user', id: '3' }
					}
				]
			});
		}
	});

	test('Loading a record with relationships works properly', function() {
		expect(3);

		var user = store.getRecord('user', '1');
		deepEqual(user.get('_vertices').mapBy('id').sort(), ['1', '2', '3'].sort());

		var vertex = store.getRecord('vertex', '2');
		deepEqual(vertex.get('_tags').mapBy('id').sort(), ['2', '4', '6'].sort());
		deepEqual(vertex.get('_owner'), { type: 'user', id: '1' });
	});

	test('Default relationship values are populated properly', function() {
		expect(2);

		var user = store.getRecord('user', '2');
		deepEqual(user.get('_vertices'), [{ type: 'vertex', id: '0' }]);

		var vertex = store.getRecord('vertex', '4');
		deepEqual(vertex.get('_tags'), []);
	});

	test('Leaving out a required relationship causes an exception', function() {
		expect(1);

		throws(function() {
			store.pushPayload({
				vertex: [{ id: '-1' }]
			});
		});
	});

	test('Changing a read-only relationship causes an exception', function() {
		expect(1);

		var vertex = store.getRecord('vertex', '4');

		throws(function() {
			vertex.clearHasOneRelationship('owner');
		});
	});

	test('Changing a read-only relationship on a new record succeeds', function() {
		expect(1);

		var vertex = store.createRecord('vertex');
		vertex.setHasOneRelationship('owner', '2');
		deepEqual(vertex.get('_owner'), { type: 'user', id: '2' });
	});
})();

<|start_filename|>tasks/convert_documentation_data.js<|end_filename|>
/* eslint-disable camelcase */
'use strict';

var fs = require('fs');
var marked = require('marked');
var renderer = new marked.Renderer();
var Handlebars = require('handlebars');

marked.setOptions({
	renderer: renderer,
	highlight: function(code) {
		return require('highlight.js').highlightAuto(code).value;
	}
});

module.exports = function(grunt) {
	grunt.registerTask('convert_documentation_data', function() {
		var json = JSON.parse(fs.readFileSync('doc/data.json', { encoding: 'utf8' }));

		var data = {
			namespaces: extractNamespaces(json),
			classes: extractClasses(json).map(function(klass) {
				klass.methods = extractClassMethods(json, getInheritanceChain(json, klass.name));
				klass.properties = extractClassProperties(json, getInheritanceChain(json, klass.name));
				return klass;
			}).filter(function(klass) {
				return klass.name !== 'EG';
			})
		};

		fs.writeFileSync('doc/ember-graph.json', JSON.stringify(data));
	});
};

function extractNamespaces(data) {
	var namespaces = [];

	data.classitems.forEach(function(item) {
		if (!item.namespace) {
			return;
		}

		if (namespaces.indexOf(item.namespace) < 0) {
			namespaces.push(item.namespace);
		}
	});

	return namespaces.sort().map(function(namespace) {
		return extractNamespace(data, namespace);
	});
}

function extractNamespace(data, namespace) {
	return {
		name: namespace,
		description: '',
		methods: extractNamespaceMethods(data, namespace),
		properties: extractNamespaceProperties(data, namespace)
	};
}

function extractNamespaceMethods(data, namespace) {
	return extractNamespaceItems(data, namespace, 'method');
}

function extractNamespaceProperties(data, namespace) {
	return extractNamespaceItems(data, namespace, 'property');
}

function extractNamespaceItems(data, namespace, type) {
	return data.classitems.filter(function(item) {
		return (item.itemtype === type && item.namespace === namespace);
	}).map(function(item) {
		item.class = null;
		return (type === 'method' ? convertMethodItem : convertPropertyItem)(item);
	}).sort(function(a, b) {
		return (a.name < b.name ? -1 : 1);
	});
}

function extractClasses(data) {
	return Object.keys(data.classes).sort().filter(function(className) {
		return !data.classes[className].namespace;
	}).map(function(className) {
		return convertClassItem(data.classes[className]);
	});
}

function getInheritanceChain(data, className) {
	var superClass = data.classes[className].extends;

	if (data.classes[superClass]) {
		var chain = getInheritanceChain(data, superClass);
		chain.push(className);
		return chain;
	} else {
		return [className];
	}
}

function extractClassMethods(data, classChain, methods) {
	return extractClassItems(data, classChain, 'method', {});
}

function extractClassProperties(data, classChain) {
	return extractClassItems(data, classChain, 'property', {});
}

function extractClassItems(data, classChain, type, items) {
	if (classChain.length <= 0) {
		return Object.keys(items).sort().map(function(key) {
			return items[key];
		});
	}

	var classItems = data.classitems.filter(function(item) {
		return (item.itemtype === type && item.class === classChain[0]);
	});

	classItems.forEach(function(item) {
		items[item.name] = (type === 'method' ? convertMethodItem : convertPropertyItem)(item);
	});

	var chainItems = extractClassItems(data, classChain.slice(1), type, items);
	return chainItems.filter(function(item) {
		return (item.private === false || item.defined_in === classChain[classChain.length - 1]);
	});
}

function convertClassItem(item) {
	return {
		name: item.name,
		'extends': item.extends || null,
		uses: item.uses || [],
		description: templateAndParseText(item.description || ''),
		deprecated: item.deprecated === true,
		file: {
			path: item.file,
			line: item.line
		}
	};
}

function convertMethodItem(item) {
	return {
		name: item.name,
		description: templateAndParseText(item.description || ''),
		'static': item.static === 1,
		deprecated: item.deprecated === true,
		parameters: (item.params || []).map(function(param) {
			param.description = templateAndParseText(param.description || '');
			return param;
		}),
		'return': item.return,
		defined_in: item.class,
		'public': (item.access !== 'protected' && item.access !== 'private'),
		'protected': item.access === 'protected',
		'private': item.access === 'private',
		abstract: (item.category || []).indexOf('abstract') >= 0,
		file: {
			path: item.file,
			line: item.line
		}
	};
}

function convertPropertyItem(item) {
	return {
		name: item.name,
		description: templateAndParseText(item.description || ''),
		type: item.type,
		'static': item.static === 1,
		deprecated: item.deprecated === true,
		readOnly: item.final === 1,
		'default': item.default,
		defined_in: item.class,
		'public': (item.access !== 'protected' && item.access !== 'private'),
		'protected': item.access === 'protected',
		'private': item.access === 'private',
		file: {
			path: item.file,
			line: item.line
		}
	};
}

function templateAndParseText(text) {
	text = text || '';
	return marked(Handlebars.compile(text)());
}
/* eslint-enable camelcase */

<|start_filename|>src/serializer/ember_graph.js<|end_filename|>
import JSONSerializer from 'ember-graph/serializer/json';

/**
 * @class EmberGraphSerializer
 * @extends JSONSerializer
 */
export default JSONSerializer.extend({
	polymorphicRelationships: true
});

<|start_filename|>src/adapter/ember_graph/load.js<|end_filename|>
import Ember from 'ember';
import Model from 'ember-graph/model/model';
import EmberGraphSet from 'ember-graph/util/set';
import copy from 'ember-graph/util/copy';

import { values } from 'ember-graph/util/util';

const Promise = Ember.RSVP.Promise;

var typeOf = Ember.typeOf;

export default {

	/**
	 * Determines whether or not to bootstrap the database
	 * with an initial set of data. If you want to initialize
	 * the database with data, you should override this property
	 * to return `true`. Use a computed property if deciding to
	 * initialize requires application logic.
	 *
	 * @method shouldInitializeDatabase
	 * @return {Boolean}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	shouldInitializeDatabase: function() {
		return false;
	},

	/**
	 * If {{link-to-method 'EmberGraphAdapter' 'shouldInitializeDatabase'}} returns `true`,
	 * then this hook is called to get the data to inject into the database. You should
	 * return your initial data payload from this hook. The format of the payload is
	 * very similar to the format required by {{link-to-method 'Store' 'pushPayload'}}.
	 * But there are a few differences for the sake of terseness:
	 *
	 * - IDs can be numbers or strings, they'll be converted to strings automatically
	 * - Relationships can be just IDs or ID objects (the later for polymorphic relationships).
	 *   For example, you may use `{ id: 1, posts: [1] }` instead of `{ id: 1, posts: [{ type: 'post', id: 1 }] }`
	 * - Optional values can be left out, they'll be filled in automatically
	 *
	 * @method getInitialPayload
	 * @return {Object}
	 * @protected
	 * @for EmberGraphAdapter
	 */
	getInitialPayload: function() {
		return { records: {}, relationships: [] };
	},

	/**
	 * Initializes the database (if configured to do so). Verifies all of the data first.
	 * Because saving data to the database can be asynchronous, this function returns
	 * a promise. Your application is probably not ready to be started until this
	 * promise resolves. If your implementation of this class saves the database
	 * synchronously, this can be done during initialization. Otherwise, you'll
	 * have to figure out a way to stall your application until it completes.
	 *
	 * @method initializeDatabase
	 * @return {Promise}
	 * @for EmberGraphAdapter
	 */
	initializeDatabase: function() {
		if (!this.shouldInitializeDatabase()) {
			return Promise.resolve();
		}

		var payload = this.getInitialPayload();

		try {
			var db = this.convertAndVerifyPayload(copy(payload, true));
			return this.setDatabase(db);
		} catch (error) {
			return Promise.reject(error);
		}
	},

	/**
	 * @method convertAndVerifyPayload
	 * @param {Object} payload
	 * @return {JSON} Database object
	 * @private
	 * @for EmberGraphAdapter
	 */
	convertAndVerifyPayload: function(payload) {
		var database = {
			records: this.extractRecords(payload),
			relationships: this.extractRelationships(payload)
		};

		this.validateDatabase(database);

		return database;
	},

	/**
	 * @method extractRecords
	 * @param {Object} payload
	 * @return {JSON} `records` object for database
	 * @private
	 * @for EmberGraphAdapter
	 */
	extractRecords: function(payload) {
		var store = this.get('store');
		var databaseRecords = {};

		values(payload, function(typeKey, records) {
			databaseRecords[typeKey] = {};

			var model = store.modelFor(typeKey);

			records.forEach(function(record) {
				databaseRecords[typeKey][record.id] = this.convertRecord(model, record);
			}, this);
		}, this);

		return databaseRecords;
	},

	/**
	 * Takes a single record, fills in missing attributes
	 * and serializes it for storage in the database.
	 *
	 * @method convertRecord
	 * @param {Class} model
	 * @param {Object} record
	 * @return {JSON}
	 * @private
	 * @for EmberGraphAdapter
	 */
	convertRecord: function(model, record) {
		var json = {};

		model.eachAttribute(function(name, meta) {
			var type = this.get('store').attributeTypeFor(meta.type);

			if (record[name] === undefined) {
				if (meta.isRequired) {
					throw new Ember.Error(Ember.get(model, 'typeKey') + ':' + record.id + ' is missing `' + name + '`');
				} else {
					json[name] = type.serialize(meta.getDefaultValue());
				}
			} else {
				json[name] = type.serialize(record[name]);
			}
		}, this);

		return json;
	},

	/**
	 * @method extractRelationships
	 * @param {Object} payload
	 * @return {JSON[]}
	 * @private
	 * @for EmberGraphAdapter
	 */
	extractRelationships: function(payload) {
		var store = this.get('store');
		var relationships = [];
		var createdRelationships = EmberGraphSet.create();

		function addRelationship(r) {
			var one = r.t1 + ':' + r.i1 + ':' + r.n1;
			var two = r.t2 + ':' + r.i2 + ':' + r.n2;
			var sorted = (one < two ? one + '::' + two : two  + '::' + one);

			if (!createdRelationships.contains(sorted)) {
				createdRelationships.addObject(sorted);
				relationships.push(r);
			}
		}

		values(payload, function(typeKey, records) {
			var model = store.modelFor(typeKey);

			records.forEach(function(record) {
				var recordRelationships = this.extractRelationshipsFromRecord(model, record);
				recordRelationships.forEach(addRelationship);
			}, this);
		}, this);

		return relationships;
	},

	extractRelationshipsFromRecord: function(model, record) {
		var relationships = [];
		var typeKey = Ember.get(model, 'typeKey');

		model.eachRelationship(function(name, meta) {
			var value = record[name];

			if (value === undefined) {
				if (meta.isRequired) {
					throw new Ember.Error(typeKey + ':' + record.id + ' is missing `' + name + '`');
				} else {
					value = meta.getDefaultValue();
				}
			}

			if (meta.kind === Model.HAS_ONE_KEY) {
				if (value !== null) {
					if (typeOf(value) === 'string' || typeOf(value) === 'number') {
						value = { type: meta.relatedType, id: value + '' };
					}

					relationships.push({
						t1: typeKey, i1: record.id + '', n1: name,
						t2: value.type, i2: value.id + '', n2: meta.inverse
					});
				}
			} else {
				value.forEach(function(other) {
					let otherRecord = other;

					if (typeOf(otherRecord) === 'string' || typeOf(otherRecord) === 'number') {
						otherRecord = { type: meta.relatedType, id: otherRecord + '' };
					}

					relationships.push({
						t1: typeKey, i1: record.id + '', n1: name,
						t2: otherRecord.type, i2: otherRecord.id + '', n2: meta.inverse
					});
				});
			}
		});

		return relationships;
	},

	/**
	 * @method validateDatabase
	 * @param {JSON} db
	 * @private
	 * @for EmberGraphAdapter
	 */
	validateDatabase: function(db) {
		function filterRelationships(typeKey, id, name) {
			return db.relationships.filter(function(r) {
				return ((r.t1 === typeKey && r.i1 === id && r.n1 === name) ||
					(r.t2 === typeKey && r.i2 === id && r.n2 === name));
			});
		}

		function relationshipToString(r) {
			var one = r.t1 + ':' + r.i1 + ':' + r.n1;
			var two = r.t2 + ':' + r.i2 + ':' + r.n2;
			return (one < two ? one + '::' + two : two  + '::' + one);
		}

		var relationshipSet = EmberGraphSet.create();
		relationshipSet.addObjects(db.relationships.map(relationshipToString));
		if (Ember.get(relationshipSet, 'length') !== db.relationships.length) {
			throw new Ember.Error('An invalid set of relationships was generated.');
		}

		db.relationships.forEach(function(relationship) {
			if (!db.records[relationship.t1][relationship.i1]) {
				throw new Ember.Error(relationship.t1 + ':' + relationship.i1 + ' doesn\'t exist');
			}

			if (!db.records[relationship.t2][relationship.i2]) {
				throw new Ember.Error(relationship.t2 + ':' + relationship.i2 + ' doesn\'t exist');
			}
		});

		values(db.records, function(typeKey, records) {
			var model = this.get('store').modelFor(typeKey);

			model.eachRelationship(function(name, meta) {
				if (meta.kind !== Model.HAS_ONE_KEY) {
					return;
				}

				values(records, function(id, record) {
					var relationships = filterRelationships(typeKey, id, name);

					if (relationships.length > 1) {
						throw new Ember.Error('Too many relationships connected to ' + typeKey + ':' + id + ':' + name);
					}
				});
			});
		}, this);
	}

};


<|start_filename|>src/store/record_request_cache.js<|end_filename|>
import Ember from 'ember';

export default Ember.Object.extend({

	cache: null,

	initializeCache: Ember.on('init', function() {
		this.set('cache', Ember.Object.create());
	}),

	_getAndCreateTypeCache(typeKey) {
		if (!this.get('cache.' + typeKey)) {
			const cache = Ember.Object.create({
				all: null,
				single: {},
				multiple: {},
				query: {}
			});

			this.set(`cache.${typeKey}`, cache);
		}

		return this.get(`cache.${typeKey}`);
	},

	savePendingRequest(typeKey /* options, request */) { // eslint-disable-line no-inline-comments
		const options = (arguments.length > 2 ? arguments[1] : undefined);
		const request = (arguments.length > 2 ? arguments[2] : arguments[1]);

		switch (Ember.typeOf(options)) {
			case 'string':
			case 'number':
				this._savePendingSingleRequest(typeKey, options + '', request);
				break;
			case 'array':
				this._savePendingManyRequest(typeKey, options.toArray(), request);
				break;
			case 'object':
				this._savePendingQueryRequest(typeKey, options, request);
				break;
			case 'undefined':
				this._savePendingAllRequest(typeKey, request);
				break;
		}
	},

	_savePendingSingleRequest(typeKey, id, request) {
		const cache = this._getAndCreateTypeCache(typeKey).get('single');

		cache[id] = request;

		const callback = () => {
			cache[id] = null;
		};

		request.then(callback, callback);
	},

	_savePendingManyRequest(typeKey, ids, request) {
		const cache = this._getAndCreateTypeCache(typeKey).get('multiple');
		const idString = ids.map((id) => id + '').sort().join(',');

		cache[idString] = request;

		const callback = () => {
			cache[idString] = null;
		};

		request.then(callback, callback);
	},

	_savePendingQueryRequest(typeKey, query, request) {
		// TODO
	},

	_savePendingAllRequest(typeKey, request) {
		const cache = this._getAndCreateTypeCache(typeKey);

		cache.set('all', request);

		const callback = () => {
			cache.set('all', null);
		};

		request.then(callback, callback);
	},

	getPendingRequest(typeKey, options) {
		switch (Ember.typeOf(options)) {
			case 'string':
			case 'number':
				return this._getPendingSingleRequest(typeKey, options + '');
			case 'array':
				return this._getPendingManyRequest(typeKey, options.toArray());
			case 'object':
				return this._getPendingQueryRequest(typeKey, options);
			case 'undefined':
				return this._getPendingAllRequest(typeKey);
			default:
				return null;
		}
	},

	_getPendingSingleRequest(typeKey, id) {
		const cache = this._getAndCreateTypeCache(typeKey);

		const all = cache.get('all');
		if (all) {
			return all;
		}

		const single = cache.get('single')[id];
		if (single) {
			return single;
		}

		const multiple = cache.get('multiple');
		for (let key in multiple) {
			if (multiple.hasOwnProperty(key)) {
				if (key.split(',').indexOf(id) >= 0) {
					return multiple[key];
				}
			}
		}

		return null;
	},

	_getPendingManyRequest(typeKey, ids) {
		const cache = this._getAndCreateTypeCache(typeKey);

		const all = cache.get('all');
		if (all) {
			return all;
		}

		const idString = ids.map((id) => id + '').sort().join(',');

		const multiple = cache.get('multiple');
		for (var key in multiple) {
			if (multiple.hasOwnProperty(key)) {
				if (key === idString) {
					return multiple[key];
				}
			}
		}

		return null;
	},

	_getPendingQueryRequest(typeKey, query) {
		// TODO
		return null;
	},

	_getPendingAllRequest(typeKey) {
		var cache = this._getAndCreateTypeCache(typeKey);
		return cache.get('all') || null;
	}

});

<|start_filename|>Gruntfile.js<|end_filename|>
/* eslint-env node */
'use strict';

var config = function(moduleName) {
	return require('./tasks/options/' + moduleName);
};

module.exports = function(grunt) {
	grunt.initConfig({
		pkg: grunt.file.readJSON('package.json'),

		build_test_runner: { // eslint-disable-line camelcase
			all: ['test/**/*.js']
		},

		build_release_test_runner: { // eslint-disable-line camelcase
			all: ['test/**/*.js']
		},

		clean: config('clean'),
		connect: config('connect'),
		eslint: config('eslint'),
		groundskeeper: config('groundskeeper'),
		qunit: config('qunit'),
		sass: config('sass'),
		uglify: config('uglify'),
		watch: config('watch'),
		yuidoc: config('yuidoc')
	});

	grunt.loadNpmTasks('grunt-contrib-clean');
	grunt.loadNpmTasks('grunt-contrib-connect');
	grunt.loadNpmTasks('grunt-contrib-uglify');
	grunt.loadNpmTasks('grunt-contrib-watch');
	grunt.loadNpmTasks('grunt-contrib-yuidoc');
	grunt.loadNpmTasks('grunt-eslint');
	grunt.loadNpmTasks('grunt-groundskeeper');
	grunt.loadNpmTasks('grunt-qunit-istanbul');
	grunt.loadNpmTasks('grunt-sass');

	grunt.task.loadTasks('./tasks');

	grunt.registerTask('develop', ['transpile', 'build_test_runner', 'connect:test', 'watch']);
	grunt.registerTask('test', ['transpile', 'build_test_runner', 'qunit:all', 'clean:test']);
	grunt.registerTask('release', ['transpile', 'groundskeeper:compile',
		'uglify:release', 'build_release_test_runner']);

	grunt.registerTask('build_site', ['yuidoc', 'register_handlebars_helpers', 'convert_documentation_data',
		'setup_site_structure', 'sass', 'build_api_pages']);
};


<|start_filename|>src/adapter/adapter.js<|end_filename|>
import Ember from 'ember';

import { abstractMethod } from 'ember-graph/util/util';

/**
 * An interface for an adapter. And adapter is used to communicate with
 * the server. The adapter is never called directly, its methods are
 * called by the store to perform its operations.
 *
 * The adapter should return normalized JSON from its operations. Details
 * about normalized JSON can be found in the {{link-to-method 'Store' 'pushPayload'}}
 * documentation.
 *
 * @class Adapter
 * @constructor
 * @category abstract
 */
export default Ember.Object.extend({

	/**
	 * The store that this adapter belongs to.
	 * This might be needed to get models and their metadata.
	 *
	 * @property store
	 * @type Store
	 * @final
	 */
	store: null,

	/**
	 * Persists a record to the server. The returned JSON
	 * must include the `newId` meta attribute as described
	 * {{link-to-method 'here' 'Serializer' 'deserialize'}}.
	 *
	 * @method createRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	createRecord: abstractMethod('createRecord'),

	/**
	 * Fetch a record from the server.
	 *
	 * @method findRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	findRecord: abstractMethod('findRecord'),

	/**
	 * The same as find, only it should load several records.
	 *
	 * @method findMany
	 * @param {String} typeKey
	 * @param {String[]} ids
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	findMany: abstractMethod('findMany'),

	/**
	 * The same as find, only it should load all records of the given type.
	 *
	 * @method findAll
	 * @param {String} typeKey
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	findAll: abstractMethod('findAll'),

	/**
	 * Queries the server for records of the given type. The resolved
	 * JSON should include the `queryIds` meta attribute as
	 * described {{link-to-method 'here' 'Serializer' 'deserialize'}}.
	 *
	 * @method findQuery
	 * @param {String} typeKey
	 * @param {Object} query The query object passed into the store's `find` method
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	findQuery: abstractMethod('findQuery'),

	/**
	 * Saves the record's changes to the server.
	 *
	 * @method updateRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	updateRecord: abstractMethod('updateRecord'),

	/**
	 * Deletes the record.
	 *
	 * @method deleteRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the normalized JSON
	 * @category abstract
	 */
	deleteRecord: abstractMethod('deleteRecord'),

	/**
	 * Gets the serializer specified for a type. It first tries to get
	 * a type-specific serializer. If it can't find one, it tries to use
	 * the application serializer. If it can't find one, it uses the default
	 * {{link-to-class 'JSONSerializer'}}.
	 *
	 * @method serializerFor
	 * @param {String} typeKey
	 * @return {Serializer}
	 * @protected
	 */
	serializerFor: function(typeKey) {
		return this.get('store').serializerFor(typeKey);
	},

	/**
	 * Serializes the given record. By default, it defers to the serializer.
	 *
	 * @method serialize
	 * @param {Model} record
	 * @param {Object} options
	 * @return {Object} Serialized record
	 * @protected
	 */
	serialize: function(record, options) {
		return this.serializerFor(record.get('typeKey')).serialize(record, options);
	},

	/**
	 * Deserializes the given payload. By default, it defers to the serializer.
	 *
	 * @method deserialize
	 * @param {JSON} payload
	 * @param {Object} options
	 * @return {Object} Normalized JSON payload
	 * @protected
	 */
	deserialize: function(payload, options) {
		return this.serializerFor(options.recordType).deserialize(payload, options);
	}
});


<|start_filename|>src/adapter/ember_graph/adapter.js<|end_filename|>
import Ember from 'ember';
import Adapter from 'ember-graph/adapter/adapter';
import LoadMethods from 'ember-graph/adapter/ember_graph/load';
import ServerMethods from 'ember-graph/adapter/ember_graph/server';
import DatabaseMethods from 'ember-graph/adapter/ember_graph/database';

import { computed } from 'ember-graph/util/computed';

var Promise = Ember.RSVP.Promise;

/**
 * This class acts as a base adapter for synchronous storage forms. Specifically,
 * the {{link-to-class 'LocalStorageAdapter'}} and {{link-to-class 'MemoryAdapter'}}
 * inherit from this class. This class will perform all of the work of updating data
 * and maintaining data integrity, subclasses need only implement the
 * {{link-to-method 'EmberGraphAdapter' 'getDatabase'}} and
 * {{link-to-method 'EmberGraphAdapter' 'setDatabase'}} methods to create a
 * fully-functioning adapter. This class works with data as a single JSON object
 * that takes the following form:
 *
 * ```json
 * {
 *     "records": {
 *          "type_key": {
 *              "record_id": {
 *                  "attr1": "value",
 *                  "attr2": 5
 *              }
 *          }
 *     },
 *     "relationships": [{
 *             "t1": "type_key",
 *             "i1": "id",
 *             "n1": "relationship_name",
 *             "t2": "type_key",
 *             "i2": "id",
 *             "n2": "relationship_name",
 *     }]
 * }
 * ```
 *
 * If you can store the JSON data, then this adapter will ensure complete
 * database integrity, since everything is done is single transactions.
 * You may also override some of the hooks and methods if you wish to
 * customize how the adapter saves or retrieves data.
 *
 * @class EmberGraphAdapter
 * @extends Adapter
 * @category abstract
 */
var EmberGraphAdapter = Adapter.extend({

	/**
	 * Since we control both the client and 'server', we'll
	 * use the same serializer for all records.
	 *
	 * @property serializer
	 * @type JSONSerializer
	 * @protected
	 * @final
	 */
	serializer: computed({
		get() {
			return Ember.getOwner(this).lookup('serializer:ember_graph');
		}
	}),

	createRecord: function(record) {
		var _this = this;
		var typeKey = record.get('typeKey');
		var serializerOptions = { requestType: 'createRecord', recordType: typeKey };
		var json = this.serialize(record, serializerOptions);

		return this.serverCreateRecord(typeKey, json).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	findRecord: function(typeKey, id) {
		var _this = this;
		var serializerOptions = { requestType: 'findRecord', recordType: typeKey };

		return this.serverFindRecord(typeKey, id).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	findMany: function(typeKey, ids) {
		var _this = this;
		var serializerOptions = { requestType: 'findMany', recordType: typeKey };

		return this.serverFindMany(typeKey, ids).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	findAll: function(typeKey) {
		var _this = this;
		var serializerOptions = { requestType: 'findAll', recordType: typeKey };

		return this.serverFindAll(typeKey).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	findQuery: function() {
		return Promise.reject('LocalStorageAdapter doesn\'t implement `findQuery` by default.');
	},

	updateRecord: function(record) {
		var _this = this;
		var typeKey = record.get('typeKey');
		var serializerOptions = { requestType: 'updateRecord', recordType: typeKey };
		var changes = this.serialize(record, serializerOptions);

		return this.serverUpdateRecord(typeKey, record.get('id'), changes).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	deleteRecord: function(record) {
		var _this = this;
		var typeKey = record.get('typeKey');
		var serializerOptions = { requestType: 'deleteRecord', recordType: typeKey };

		return this.serverDeleteRecord(typeKey, record.get('id')).then(function(payload) {
			return _this.deserialize(payload, serializerOptions);
		});
	},

	serialize: function(record, options) {
		return this.get('serializer').serialize(record, options);
	},

	deserialize: function(payload, options) {
		return this.get('serializer').deserialize(payload, options);
	}

});

EmberGraphAdapter.reopen(LoadMethods);
EmberGraphAdapter.reopen(ServerMethods);
EmberGraphAdapter.reopen(DatabaseMethods);

export default EmberGraphAdapter;


<|start_filename|>src/relationship/relationship_store.js<|end_filename|>
import Ember from 'ember';
import Relationship from 'ember-graph/relationship/relationship';
import EmberGraphSet from 'ember-graph/util/set';


var CLIENT_STATE = Relationship.CLIENT_STATE;
var SERVER_STATE = Relationship.SERVER_STATE;
var DELETED_STATE = Relationship.DELETED_STATE;

var STATE_MAP = {};
STATE_MAP[CLIENT_STATE] = 'client';
STATE_MAP[SERVER_STATE] = 'server';
STATE_MAP[DELETED_STATE] = 'deleted';

var RelationshipMap = Ember.Object.extend({

	length: 0,

	addRelationship: function(name, relationship) {
		if (this.hasOwnProperty(name)) {
			this.set(name + '.' + relationship.get('id'), relationship);
			this.notifyPropertyChange(name);
		} else {
			var o = Ember.Object.create();
			o.set(relationship.get('id'), relationship);
			this.set(name, o);
		}

		this.incrementProperty('length');
	},

	removeRelationship: function(id) {
		Object.keys(this).forEach(function(key) {
			if (key === 'length') {
				return;
			}

			var o = this.get(key);
			if (typeof o === 'object' && o.hasOwnProperty(id)) {
				delete o[id];
				this.notifyPropertyChange(key);
				this.decrementProperty('length');
			}
		}, this);
	},

	getRelationships: function(name) {
		var relationships = this.get(name) || {};

		return Object.keys(relationships).map(function(key) {
			return relationships[key];
		});
	},

	getAllRelationships: function() {
		var relationships = [];
		var keys = EmberGraphSet.create();
		keys.addObjects(Object.keys(this));
		keys = keys.without('length');

		keys.forEach(function(key) {
			relationships = relationships.concat(this.getRelationships(key));
		}, this);

		return relationships;
	},

	clearRelationships: function(name) {
		this.set(name, Ember.Object.create());
		this.recalculateLength();
	},

	recalculateLength: function() {
		var length = 0;

		Object.keys(this).forEach(function(key) {
			if (key !== 'length') {
				length += Object.keys(this[key]).length;
			}
		}, this);

		this.set('length', length);
	}

});

export default Ember.Object.extend({

	server: null,

	client: null,

	deleted: null,

	initializeMaps: Ember.on('init', function() {
		this.setProperties({
			server: RelationshipMap.create(),
			client: RelationshipMap.create(),
			deleted: RelationshipMap.create()
		});
	}),

	addRelationship: function(name, relationship) {
		if (name === null) {
			return;
		}

		return this.get(STATE_MAP[relationship.get('state')]).addRelationship(name, relationship);
	},

	removeRelationship: function(id) {
		if (Ember.typeOf(id) !== 'string') {
			id = Ember.get(id, 'id'); // eslint-disable-line no-param-reassign
		}

		this.get('server').removeRelationship(id);
		this.get('client').removeRelationship(id);
		this.get('deleted').removeRelationship(id);
	},

	clearRelationships: function(name) {
		this.get('server').clearRelationships(name);
		this.get('client').clearRelationships(name);
		this.get('deleted').clearRelationships(name);
	},

	getServerRelationships: function(name) {
		return this.get('server').getRelationships(name).concat(this.get('deleted').getRelationships(name));
	},

	getCurrentRelationships: function(name) {
		return this.get('server').getRelationships(name).concat(this.get('client').getRelationships(name));
	},

	getRelationshipsByState: function(state) {
		return this.get(STATE_MAP[state]).getAllRelationships();
	},

	getRelationshipsByName: function(name) {
		var server = this.get('server').getRelationships(name);
		var client = this.get('client').getRelationships(name);
		var deleted = this.get('deleted').getRelationships(name);

		return server.concat(client).concat(deleted);
	}
});

<|start_filename|>test/configuration.js<|end_filename|>
(function() {
	'use strict';

	window.setupStore = function(models, options, store) {
		options = options || {};

		var container;
		var registry;

		if (Ember.Registry) {
			registry = new Ember.Registry();
			container = registry.container();
		} else {
			container = new Ember.Container();
			registry = container;
		}

		registry.register('adapter:rest', EG.RESTAdapter, { singleton: true });
		registry.register('adapter:memory', EG.MemoryAdapter, { singleton: true });
		registry.register('adapter:local_storage', EG.LocalStorageAdapter, { singleton: true });

		registry.register('serializer:json', EG.JSONSerializer, { singleton: true });
		registry.register('serializer:ember_graph', EG.EmberGraphSerializer, { singleton: true });

		registry.register('type:string', EG.StringType, { singleton: true });
		registry.register('type:number', EG.NumberType, { singleton: true });
		registry.register('type:boolean', EG.BooleanType, { singleton: true });
		registry.register('type:date', EG.DateType, { singleton: true });
		registry.register('type:object', EG.ObjectType, { singleton: true });
		registry.register('type:array', EG.ArrayType, { singleton: true });

		registry.register('store:main', store || EG.Store, { singleton: true });
		store = container.lookup('store:main');

		registry.injection('adapter', 'store', 'store:main');
		registry.injection('serializer', 'store', 'store:main');

		if (options.adapter) {
			registry.register('adapter:application', options.adapter, { singleton: true });
		}

		Object.keys(models || {}).forEach(function(typeKey) {
			registry.register('model:' + typeKey, models[typeKey]);
			// Load the model to set the 'typeKey' attributes on it
			store.modelFor(typeKey);
		});

		store.__registry__ = registry;

		return store;
	};
})();

<|start_filename|>test/adapter/rest.js<|end_filename|>
(function() {
	'use strict';

	var Promise = Ember.RSVP.Promise;
	var store;
	var adapter;

	module('REST Adapter', {
		setup: function() {
			store = setupStore({
				person: EG.Model.extend({
					name: EG.attr({ type: 'string', defaultValue: '' })
				})
			}, {
				adapter: EG.RESTAdapter.extend({
					prefix: Em.computed(function() {
						return '/api';
					})
				})
			});

			adapter = store.get('container').lookup('adapter:application');

			store.pushPayload({
				person: [{ id: '1' }]
			});
		}
	});

	test('The serializer is initialized properly', function() {
		expect(2);

		ok(EG.JSONSerializer.detectInstance(adapter.serializerFor('person')));
		ok(EG.JSONSerializer.detectInstance(adapter.serializerFor('foobar')));
	});

	asyncTest('findRecord request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people/10');
			strictEqual(verb, 'GET');
			strictEqual(body, undefined);

			return Em.RSVP.resolve();
		};

		adapter.findRecord('person', '10');
	});

	asyncTest('findMany request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people/5,6,7');
			strictEqual(verb, 'GET');
			strictEqual(body, undefined);

			return Em.RSVP.resolve();
		};

		adapter.findMany('person', ['5', '6', '7']);
	});

	asyncTest('findAll request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people');
			strictEqual(verb, 'GET');
			strictEqual(body, undefined);

			return Em.RSVP.resolve();
		};

		adapter.findAll('person');
	});

	asyncTest('findRecord request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people?searchTerm=none&sortAscending=true');
			strictEqual(verb, 'GET');
			strictEqual(body, undefined);

			return Em.RSVP.resolve();
		};

		adapter.findQuery('person', { searchTerm: 'none', sortAscending: true });
	});

	asyncTest('createRecord request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people');
			strictEqual(verb, 'POST');
			deepEqual(body, { people: [{ name: '', links: {} }] });

			return Em.RSVP.resolve({ people: [{ id: '100' }] });
		};

		var person = store.createRecord('person', { name: '' });
		adapter.createRecord(person);
	});

	asyncTest('updateRecord request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people/1');
			strictEqual(verb, 'PATCH');
			deepEqual(body, [{ op: 'replace', path: '/name', value: 'Bob' }]);

			return Em.RSVP.resolve();
		};

		var person = store.getRecord('person', '1');
		person.set('name', 'Bob');
		adapter.updateRecord(person);
	});

	asyncTest('deleteRecord request', function() {
		expect(3);

		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, '/api/people/1');
			strictEqual(verb, 'DELETE');
			strictEqual(body, undefined);

			return Em.RSVP.resolve();
		};

		var person = store.getRecord('person', '1');
		adapter.deleteRecord(person);
	});

	test('mergeRecords', function() {
		expect(1);

		var one = {
			users: [{ id: 1 }, { id: 2 }],
			posts: [{ id: 1 }],
			links: {
				posts: [{ id: 5 }]
			},
			meta: {
				prop: null
			}
		};

		var two = {
			users: [{ id: 3 }, { id: 4 }],
			posts: [],
			comments: [{ id: 1 }],
			links: {
				posts: [{ id: 2 }],
				comments: [{ id: 2 }, { id: 3 }]
			},
			meta: {
				prop: 'value'
			}
		};

		var three = {
			users: [],
			posts: [],
			comments: [],
			links: {
				users: [],
				posts: [],
				comments: []
			}
		};

		var expected = {
			users: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }],
			posts: [{ id: 1 }],
			comments: [{ id: 1 }],
			links: {
				users: [],
				posts: [{ id: 5 }, { id: 2 }],
				comments: [{ id: 2 }, { id: 3 }]
			},
			meta: {
				prop: 'value'
			}
		};

		deepEqual(adapter.mergePayloads([one, two, three]), expected);
	});

	test('buildMultipleUrls', function() {
		expect(1);

		adapter.set('urlLengthLimit', window.location.origin.length + '/api/users/'.length + 7);

		var urls = adapter.buildMultipleUrls('user', ['100', '101', '102', '103', '104', '105', '106']);

		var expected = [
			'/api/users/100,101',
			'/api/users/102,103',
			'/api/users/104,105',
			'/api/users/106'
		];

		deepEqual(urls, expected);
	});

	asyncTest('findMany with split URLs', function() {
		expect(4);

		adapter.set('urlLengthLimit', window.location.origin.length + '/api/users/'.length + 7);

		var ajaxCount = 0;
		var expectedUrls = [
			'/api/users/100,101',
			'/api/users/102,103',
			'/api/users/104,105',
			'/api/users/106'
		];
		adapter.ajax = function(url, verb, body) {
			start();

			strictEqual(url, expectedUrls[ajaxCount]);

			ajaxCount++;
			if (ajaxCount < 4) {
				stop();
			}

			return Promise.resolve({});
		};

		adapter.findMany('user', ['100', '101', '102', '103', '104', '105', '106']);
	});
})();


<|start_filename|>test/store/save.js<|end_filename|>
(function() {
	'use strict';

	var store;
	var Promise = Em.RSVP.Promise;

	module('Store Test', {
		setup: function() {
			store = setupStore({
				user: EG.Model.extend({
					email: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					posts: EG.hasMany({
						relatedType: 'post',
						inverse: 'author',
						isRequired: true
					})
				}),

				post: EG.Model.extend({
					title: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					}),

					body: EG.attr({
						type: 'string',
						isRequired: false,
						defaultValue: ''
					}),

					author: EG.hasOne({
						relatedType: 'user',
						inverse: 'posts',
						isRequired: true
					}),

					tags: EG.hasMany({
						relatedType: 'tag',
						inverse: null,
						isRequired: true
					})
				}),

				tag: EG.Model.extend({
					name: EG.attr({
						type: 'string',
						isRequired: true,
						readOnly: true
					})
				})
			}, {
				adapter: EG.Adapter.extend({
					updateRecord: function(record) {
						return Em.RSVP.Promise.resolve();
					}
				})
			});
		}
	});

	asyncTest('Can create record without `createdRecord` meta attribute', function() {
		expect(5);

		var tag = store.createRecord('tag', { name: 'tag1' });

		start();
		strictEqual(tag.get('name'), 'tag1');
		strictEqual(tag.get('isNew'), true);
		stop();

		store.adapterFor('application').createRecord = function() {
			return Promise.resolve({ tag: [{ id: '100', name: 'tag1' }] });
		};

		tag.save().then(function() {
			start();
			strictEqual(tag.get('id'), '100');
			strictEqual(tag.get('name'), 'tag1');
			strictEqual(tag.get('isNew'), false);
		});
	});

	asyncTest('Excluding `createdRecord` throws when more than one record of a type is included', function() {
		expect(1);

		store.adapterFor('application').createRecord = function() {
			return Promise.resolve({ tag: [{}, {}, {}] });
		};

		store.createRecord('tag', { name: '' }).save().catch(function(error) {
			start();
			ok(error.message.indexOf('`createdRecord`') >= 0);
		});
	});

	asyncTest('Saving without returning a payload updates the record', function() {
		expect(10);

		store.pushPayload({
			user: [{
				id: '1',
				email: 'test@test',
				posts: []
			}],
			post: [{
				id: '1',
				title: '',
				body: '',
				author: null,
				tags: []
			}],
			tag: [{
				id: '1',
				name: 'tag1'
			}]
		});

		var post = store.getRecord('post', '1');
		post.set('body', 'Test Body');
		post.setHasOneRelationship('author', '1');
		post.addToRelationship('tags', '1');

		start();
		strictEqual(post.get('title'), '');
		strictEqual(post.get('body'), 'Test Body');
		deepEqual(post.get('_author'), { type: 'user', id: '1' });
		deepEqual(post.get('_tags'), [{ type: 'tag', id: '1' }]);
		strictEqual(post.get('isDirty'), true);
		stop();

		post.save().then(function() {
			start();

			strictEqual(post.get('title'), '');
			strictEqual(post.get('body'), 'Test Body');
			deepEqual(post.get('_author'), { type: 'user', id: '1' });
			deepEqual(post.get('_tags'), [{ type: 'tag', id: '1' }]);
			strictEqual(post.get('isDirty'), false);
		});
	});
})();


<|start_filename|>src/relationship/relationship.js<|end_filename|>
import Ember from 'ember';

import { generateUUID } from 'ember-graph/util/util';
import { RelationshipStates } from 'ember-graph/constants';
import { computed } from 'ember-graph/util/computed';

var CLIENT_STATE = RelationshipStates.CLIENT_STATE;
var SERVER_STATE = RelationshipStates.SERVER_STATE;
var DELETED_STATE = RelationshipStates.DELETED_STATE;

var Relationship = Ember.Object.extend({

	_state: CLIENT_STATE,
	state: computed('_state', {
		get() {
			return this.get('_state');
		},
		set(key, value) {
			switch (value) {
				case CLIENT_STATE:
				case SERVER_STATE:
				case DELETED_STATE:
					this.set('_state', value);
					break;
				default:
					Ember.assert('Invalid relationship state: ' + value);
					break;
			}
		}
	}),

	id: null,

	type1: null,

	id1: null,

	relationship1: null,

	type2: null,

	id2: null,

	relationship2: null,

	isConnectedTo: function(record) {
		if (this.get('type1') === record.typeKey && this.get('id1') === record.get('id')) {
			return true;
		}

		if (this.get('type2') === record.typeKey && this.get('id2') === record.get('id')) {
			return true;
		}

		return false;
	},

	matchesOneSide: function(type, id, name) {
		if (this.get('type1') === type && this.get('id1') === id && this.get('relationship1') === name) {
			return true;
		}

		if (this.get('type2') === type && this.get('id2') === id && this.get('relationship2') === name) {
			return true;
		}

		return false;
	},

	otherType: function(record) {
		// If they have the same type, it won't matter which branch is taken
		if (this.get('type1') === record.typeKey) {
			return this.get('type2');
		} else {
			return this.get('type1');
		}
	},

	otherId: function(record) {
		// If they have the same IDs, it won't matter which branch is taken
		if (this.get('id1') === record.get('id')) {
			return this.get('id2');
		} else {
			return this.get('id1');
		}
	},

	otherName: function(record) {
		if (this.get('id1') === record.get('id') && this.get('type1') === record.typeKey) {
			return this.get('relationship2');
		} else {
			return this.get('relationship1');
		}
	},

	thisName: function(record) {
		if (this.get('id1') === record.get('id') && this.get('type1') === record.typeKey) {
			return this.get('relationship1');
		} else {
			return this.get('relationship2');
		}
	},

	changeId: function(typeKey, oldId, newId) {
		if (this.get('type1') === typeKey && this.get('id1') === oldId) {
			this.set('id1', newId);
		} else if (this.get('type2') === typeKey && this.get('id2') === oldId) {
			this.set('id2', newId);
		}
	},

	erase: function() {
		this.setProperties({
			id: null,
			type1: null,
			id1: null,
			relationship1: null,
			type2: null,
			id2: null,
			relationship2: null,
			_state: null
		});
	}
});

Relationship.reopenClass({
	// TODO: NEW_STATE, SAVED_STATE, DELETED_STATE
	CLIENT_STATE: CLIENT_STATE,
	SERVER_STATE: SERVER_STATE,
	DELETED_STATE: DELETED_STATE,

	create(type1, id1, relationship1, type2, id2, relationship2, state) {
		Ember.assert('Invalid type or ID', type1 && id1 && type2 && id2);
		Ember.assert('First relationship must have a name', relationship1);
		Ember.assert('Second relationship must have a name or be null',
			relationship2 === null || Ember.typeOf(relationship2) === 'string');
		Ember.assert('Invalid state', state === CLIENT_STATE || state === SERVER_STATE || state === DELETED_STATE);

		const id = generateUUID();
		return this._super({ id, type1, id1, relationship1, type2, id2, relationship2, state });
	}
});

export default Relationship;

<|start_filename|>tasks/build_test_runner.js<|end_filename|>
'use strict';

var EMBER_VERSIONS = {
	'1.8.0': {
		development: 'ember-1.8.0/ember.js',
		production: 'ember-1.8.0/ember.prod.js'
	},
	'1.13.0': {
		development: 'ember-1.13.0/ember.debug.js',
		production: 'ember-1.13.0/ember.prod.js'
	},
	'2.0.0': {
		development: 'ember-2.0.0/ember.debug.js',
		production: 'ember-2.0.0/ember.prod.js'
	}
};

module.exports = function(grunt) {
	function buildRunners(release) {
		var template = grunt.file.read('test/template.html.tmpl');

		var renderingContext = {
			data: {
				sourceFile: (release ? 'ember-graph.min.js' : 'ember-graph.js'),
				files: this.filesSrc
			}
		};

		renderingContext.data.emberFile =
			(release ? EMBER_VERSIONS['1.8.0'].production : EMBER_VERSIONS['1.8.0'].development);
		renderingContext.data.includeHandlebars = true;
		grunt.file.write('test/ember-1.8.0.html', grunt.template.process(template, renderingContext));

		renderingContext.data.emberFile =
			(release ? EMBER_VERSIONS['1.13.0'].production : EMBER_VERSIONS['1.13.0'].development);
		renderingContext.data.includeHandlebars = false;
		grunt.file.write('test/ember-1.13.0.html', grunt.template.process(template, renderingContext));

		renderingContext.data.emberFile =
				(release ? EMBER_VERSIONS['2.0.0'].production : EMBER_VERSIONS['2.0.0'].development);
		renderingContext.data.includeHandlebars = false;
		grunt.file.write('test/ember-2.0.0.html', grunt.template.process(template, renderingContext));
	}

	grunt.registerMultiTask('build_test_runner', function() {
		buildRunners.call(this, false);
	});

	grunt.registerMultiTask('build_release_test_runner', function() {
		buildRunners.call(this, true);
	});
};

<|start_filename|>src/relationship/relationship_hash.js<|end_filename|>
import Ember from 'ember';

/**
 * Define HashMap class
 *
 * Simple use of associative array as hash table allowing buckets w/
 * chaining for duplicates.
 */

var RelationshipNode = Ember.Object.extend({

	next: undefined,
	prev: undefined,
	item: undefined,

	initialize: Ember.on('init', function() {
		this.setProperties({
			next: undefined,
			prev: undefined,
			item: undefined
		});
	})

});


var RelationshipHash = Ember.Object.extend({

	buckets: {},

	initialize: Ember.on('init', function() {
		this.setProperties({
			buckets: []
		});
	}),

	add(item, ids) {
		for (var i = 0; i < ids.length; ++i) {
			var newNode = RelationshipNode.create();
			newNode.item = item;
			if (this.buckets[ids[i]]) {
				// Push onto head of chain
				newNode.next = this.buckets[ids[i]];
				this.buckets[ids[i]].prev = newNode;
			}
			this.buckets[ids[i]] = newNode;
		}
	},

	findAllByKeys(ids) {
		var retVal = [];
		for (var i = 0; i < ids.length; ++i) {
			var current = this.buckets[ids[i]];
			while (current) {
				retVal[current.item.id] = current.item;
				current = current.next;
			}
		}
		return retVal;
	},

	remove(item, ids) {
		let removed;
		for (var i = 0; i < ids.length; ++i) {
			var current = this.buckets[ids[i]];
			while (current) {
				var next = current.next;
				if (current.item === item) {
					if (!current.next && !current.prev) {
						var deleteme = this.buckets[ids[i]];
						this.buckets[ids[i]] = undefined;
						removed = 1;
						// Don't forget to delete last node removed
						deleteme.destroy();
					} else {
						if (current.next) {
							current.next.prev = current.prev;
						}
						if (current.prev) {
							current.prev.next = current.next;
						} else {
							// Removing head
							this.buckets[ids[i]] = current.next;
						}
						current.prev = undefined;
						current.next = undefined;
						removed = 1;
						// Delete node when removed
						current.destroy();
					}
				}
				current = next;
			}
		}
		return removed;
	}
});

export default RelationshipHash;

<|start_filename|>src/data/promise_object.js<|end_filename|>
import Ember from 'ember';

import { computed } from 'ember-graph/util/computed';

/**
 * Ember's ObjectProxy combined with the PromiseProxyMixin.
 * Acts as an object and proxies all properties to the
 * given promise when it resolves.
 *
 * @class PromiseObject
 * @extends ObjectProxy
 * @uses PromiseProxyMixin
 * @constructor
 */
var PromiseObject = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin);

/**
 * Ember's ArrayProxy combined with the PromiseProxyMixin.
 * Acts as an array and proxies all properties to the
 * given promise when it resolves.
 *
 * @class PromiseArray
 * @extends ArrayProxy
 * @uses PromiseProxyMixin
 * @constructor
 */
var PromiseArray = Ember.ArrayProxy.extend(Ember.PromiseProxyMixin);

/**
 * Acts just like `PromiseObject` only it's able to hold the
 * ID and typeKey of a model before it's resolved completely.
 *
 * ```js
 * var user = EG.ModelPromiseObject.create({
 *     promise: this.store.find('user', '52'),
 *     id: '52',
 *     typeKey: 'user'
 * });
 *
 * user.get('isPending'); // true
 * user.get('id'); // '52'
 * user.get('typeKey'); // 'user'
 * ```
 *
 * @class ModelPromiseObject
 * @extends PromiseObject
 * @constructor
 */
var ModelPromiseObject = PromiseObject.extend({
	__modelId: null,
	__modelTypeKey: null,

	id: computed('__modelId', 'content.id', {
		get() {
			const content = this.get('content');

			if (content && content.get) {
				return content.get('id');
			} else {
				return this.get('__modelId');
			}
		},
		set(key, value) {
			const content = this.get('content');

			if (content && content.set) {
				content.set('id', value);
			} else {
				this.set('__modelId', value);
			}
		}
	}),

	typeKey: computed('__modelTypeKey', 'content.typeKey', {
		get() {
			const content = this.get('content');

			if (content && content.get) {
				return content.get('typeKey');
			} else {
				return this.get('__modelTypeKey');
			}
		},
		set(key, value) {
			const content = this.get('content');

			if (content && content.set) {
				content.set('typeKey', value);
			} else {
				this.set('__modelTypeKey', value);
			}
		}
	}),

	/**
	 * Returns the underlying model for this promise. If the promise
	 * isn't resolved yet, the model will be `undefined`.
	 *
	 * @method getModel
	 * @return {Model}
	 */
	getModel: function() {
		return this.get('content');
	},

	/**
	 * Proxies to the underlying model's `destroy` method.
	 * Will return a rejected promise if the promise isn't resolved yet.
	 *
	 * @method destroy
	 * @return {Promise}
	 */
	destroy: function() {
		var model = this.getModel();

		if (model && typeof model.destroy === 'function') {
			return model.destroy();
		} else {
			return Ember.RSVP.Promise.reject('Can\'t destroy a record that is still loading.');
		}
	}
});

export {
	PromiseObject,
	PromiseArray,
	ModelPromiseObject
};

<|start_filename|>site/javascripts/api.js<|end_filename|>
$('.api-index .nav-tabs a').click(function(e) {
	e.preventDefault();
	$(this).tab('show');
	window.location.hash = $(this).attr('href');
});

function showPane(href) {
	$('.api-index .nav-tabs a[href="' + href + '"]').tab('show');
}

$(window).on('hashchange', function() {
	var hash = window.location.hash;
	var el = $(hash);

	if (el.length > 0 && !el.is(':visible')) {
		var href = null;

		if (el.hasClass('tab-pane')) {
			href = '#' + el.attr('id');
		} else {
			href = '#' + el.parents('.tab-pane').attr('id');
		}

		showPane(href);
		$('html, body').scrollTop(el.offset().top);
	} else if (el.length === 0) {
		window.location.hash = '#index';
		showPane('#index');
	}

	// Make sure the index tab is selected if the panel is visible
	if ($('#index').is(':visible')) {
		showPane('#index');
	}
});

$(window).trigger('hashchange');

<|start_filename|>test/model/type.js<|end_filename|>
(function() {
	'use strict';

	var store = setupStore({ test: EG.Model.extend() });

	module('Model Type Property Test');

	test('typeKey exists on instances', function() {
		expect(1);

		var model = store.createRecord('test', {});

		ok(model.typeKey === 'test');
	});

	test('typeKey exists on the class', function() {
		expect(1);

		var TestModel = store.modelFor('test');

		ok(TestModel.typeKey === 'test');
	});

	test('Looking up a type from the store works', function() {
		expect(1);

		ok(EG.Model.detect(store.modelFor('test')));
	});
})();


<|start_filename|>src/util/util.js<|end_filename|>
import Ember from 'ember';
import EmberGraphSet from 'ember-graph/util/set';

import { computed } from 'ember-graph/util/computed';

/**
 * Denotes that method must be implemented in a subclass.
 * If it's not overridden, calling it will throw an error.
 *
 * ```js
 * var Shape = Ember.Object.extend({
 *     getNumberOfSides: EG.abstractMethod('getNumberOfSides')
 * });
 * ```
 *
 * @method abstractMethod
 * @param {String} methodName
 * @return {Function}
 * @namespace EmberGraph
 */
function abstractMethod(methodName) {
	return function() {
		throw new Ember.Error('You failed to implement the abstract `' + methodName + '` method.');
	};
}

/**
 * Denotes that a property must be overridden in a subclass.
 * If it's not overridden, using it will throw an error.
 *
 * ```js
 * var Shape = Ember.Object.extend({
 *     name: EG.abstractProperty('name')
 * });
 * ```
 *
 * @method abstractProperty
 * @param {String} propertyName
 * @return {ComputedProperty}
 * @namespace EmberGraph
 */
function abstractProperty(propertyName) {
	return computed({
		get() {
			throw new Ember.Error('You failed to override the abstract `' + propertyName + '` property.');
		}
	});
}

/**
 * Generates a version 4 (random) UUID.
 *
 * @method generateUUID
 * @return {String}
 * @namespace EmberGraph
 */
function generateUUID() {
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		var r = Math.random() * 16|0; // eslint-disable-line
		var v = (c == 'x' ? r : (r&0x3|0x8)); // eslint-disable-line
		return v.toString(16);
	});
}

/**
 * Compares the contents of two arrays for equality. Uses
 * Ember.Set to make the comparison, so the objects must
 * be equal with `===`.
 *
 * @method arrayContentsEqual
 * @param {Array} a
 * @param {Array} b
 * @returns {Boolean}
 * @namespace EmberGraph
 */
function arrayContentsEqual(a, b) {
	var set = EmberGraphSet.create();
	set.addObjects(a);
	return (a.length === b.length && set.isEqual(b));
}

/**
 * Takes a list of record objects (with `type` and `id`)
 * and groups them into arrays based on their type.
 *
 * @method groupRecords
 * @param {Object[]} records
 * @return {Array[]}
 * @namespace EmberGraph
 */
function groupRecords(records) {
	var groups = records.reduce(function(groups, record) {
		if (groups[record.type]) {
			groups[record.type].push(record);
		} else {
			groups[record.type] = [record];
		}

		return groups;
	}, {});

	return Object.keys(groups).reduce(function(array, key) {
		if (groups[key].length > 0) {
			array.push(groups[key]);
		}

		return array;
	}, []);
}

/**
 * Calls `callback` once for each value of the given object.
 * The callback receives `key` and `value` parameters.
 *
 * @method values
 * @param {Object} obj
 * @param {Function} callback
 * @param {Any} [thisArg=undefined]
 * @namespace EmberGraph
 */
function values(obj, callback, thisArg) {
	var keys = Object.keys(obj);

	for (var i = 0; i < keys.length; ++i) {
		callback.call(thisArg, keys[i], obj[keys[i]]);
	}
}

/**
 * Works like `Ember.aliasMethod` only it displays a
 * deprecation warning before the aliased method is called.
 *
 * @method deprecateMethod
 * @param {String} message
 * @param {String} method
 * @return {Function}
 * @namespace EmberGraph
 */
function deprecateMethod(message, method) {
	return function() {
		Ember.deprecate(message, false, { id: method, until: '2.0.0' });
		this[method].apply(this, arguments);
	};
}

/**
 * Works like 'Ember.computed.alias' only it displays a
 * deprecation warning before the aliased property is returned.
 *
 * @method deprecateProperty
 * @param {String} message
 * @param {String} property
 * @return {ComputedProperty}
 * @namespace EmberGraph
 */
function deprecateProperty(message, property) {
	return computed(property, {
		get() {
			Ember.deprecate(message, false, { id: property, until: '2.0.0' });
			return this.get(property);
		},
		set(key, value) {
			this.set(property, value);
		}
	});
}

export {
	abstractMethod,
	abstractProperty,
	generateUUID,
	arrayContentsEqual,
	groupRecords,
	values,
	deprecateMethod,
	deprecateProperty
};


<|start_filename|>src/before_load.js<|end_filename|>
var define = this.define;
var require = this.require;

var declareModuleLoader = function() {
	var DEFINITIONS = {};
	var MODULES = {};

	var evaluateModule = function(name) {
		if (!DEFINITIONS[name]) {
			throw new Error('Module not found: ' + name);
		}

		var exports = {};
		var dependencies = DEFINITIONS[name].dependencies.map(function(name) {
			if (name === 'exports') {
				return exports;
			} else {
				return require(name);
			}
		});

		DEFINITIONS[name].definition.apply(null, dependencies);

		MODULES[name] = exports;
		DEFINITIONS[name] = null;

		return exports;
	};

	define = function(name, dependencies, definition) {
		DEFINITIONS[name] = {
			dependencies: dependencies,
			definition: definition
		};
	};

	require = function(name) {
		if (!MODULES[name]) {
			MODULES[name] = evaluateModule(name);
		}

		return MODULES[name];
	};
};

var declareGlobalModule = function(global) {
	define('ember-graph', ['exports', 'ember'], function(exports, Ember) {
		/**
		 * @module ember-graph
		 * @main ember-graph
		 */
		global.EmberGraph = global.EG = Ember['default'].Namespace.create();
		exports['default'] = global.EmberGraph;
	});
};

// This is probably a poor way of detecting Ember CLI. Should work for now...
if (!define || !define.petal) {
	declareModuleLoader();
}

var global = this;

try {
	if (!require('ember')) {
		throw null;
	}
} catch (e) {
	define('ember', ['exports'], function(exports) {
		exports['default'] = global.Ember;
	});
}

try {
	if (!require('jquery')) {
		throw null;
	}
} catch (e) {
	define('jquery', ['exports'], function(exports) {
		exports['default'] = global.jQuery;
	});
}

declareGlobalModule(this);

<|start_filename|>src/util/array.js<|end_filename|>
import Ember from 'ember';

var mapBy = function(property) {
	return this.map((item) => Ember.get(item, property));
};

export { mapBy };

<|start_filename|>src/serializer/serializer.js<|end_filename|>
import Ember from 'ember';

import { abstractMethod } from 'ember-graph/util/util';

/**
 * An interface for a serializer. A serializer is used to convert
 * objects back and forth between the JSON that the server uses,
 * and the records that are used on the client side.
 *
 * @class Serializer
 */
export default Ember.Object.extend({

	/**
	 * The store that the records will be loaded into. This
	 * property is injected by the container on startup.
	 * This can be used for fetching models and their metadata.
	 *
	 * @property store
	 * @type Store
	 * @final
	 */
	store: null,

	/**
	 * Converts a record to a JSON payload that can be sent to
	 * the server. The options object is a general object where any options
	 * necessary can be passed in from the adapter. The built-in Ember-Graph
	 * adapters pass in just one option: `requestType`. This lets the
	 * serializer know what kind of request will made using the payload
	 * returned from this call. The value of `requestType` will be one of
	 * either `createRecord` or `updateRecord`. If you write a custom
	 * adapter or serializer, you're free to pass in any other options
	 * you may need.
	 *
	 * @method serialize
	 * @param {Model} record The record to serialize
	 * @param {Object} [options] Any options that were passed by the adapter
	 * @return {JSON} JSON payload to send to server
	 * @abstract
	 */
	serialize: abstractMethod('serialize'),

	/**
	 * Takes a payload from the server and converts it into a normalized
	 * JSON payload that the store can use. Details about the format
	 * can be found in the {{link-to-method 'Store' 'pushPayload'}}
	 * documentation.
	 *
	 * In addition to the format described by the store, the store
	 * may require some additional information. This information should
	 * be included in the `meta` object. The attributes required by the
	 * store are:
	 *
	 * - `matchedRecords`: This is an array of objects (with `type` and
	 *     `id` fields) that tell which records were matched on a query.
	 *     This helps distinguish queried records from records of the
	 *     same type that may have been side loaded. If this property
	 *     doesn't exist, the adapter will assume that all objects
	 *     of that type were returned by the query.
	 * - `createdRecord`: This is a single object (with `type` and `id`)
	 *     that tells the adapter which record was created as the result
	 *     of a `createRecord` request. Again, this helps distinguish
	 *     the record from other records of the same type.
	 *
	 * To determine whether those meta attributes are required or not, the
	 * `requestType` option can be used. The built-in Ember-Graph adapters
	 * will pass one of the following values: `findRecord`, `findMany`,
	 * `findAll`, `findQuery`, `createRecord`, `updateRecord`, `deleteRecord`.
	 * If the value is `findQuery`, then the `matchedRecords` meta attribute is
	 * required. If the value is `createRecord`, then the `createdRecord` meta
	 * attribute is required.
	 *
	 * In addition to `requestType`, the following options are available:
	 *
	 * - `recordType`: The type of record that the request was performed on
	 * - `id`:  The ID of the record referred to by a `findRecord`,
	 *     `updateRecord` or `deleteRecord` request
	 * - `ids`: The IDs of the records requested by a `findMany` request
	 * - `query`: The query submitted to the `findQuery` request
	 *
	 * @method deserialize
	 * @param {JSON} payload
	 * @param {Object} [options] Any options that were passed by the adapter
	 * @return {Object} Normalized JSON payload
	 * @abstract
	 */
	deserialize: abstractMethod('deserialize')
});


<|start_filename|>src/initializer.js<|end_filename|>
import Ember from 'ember';
import EmberGraph from 'ember-graph';

Ember.onLoad('Ember.Application', function(Application) {
	Application.initializer({
		name: 'ember-graph',
		initialize() {
			let application = arguments[1] || arguments[0];
			Ember.libraries.register('Ember Graph');

			const useService = !!Ember.Service;

			application.register('store:main', application.Store || EmberGraph.Store);

			if (useService) {
				application.register('service:store', application.Store || EmberGraph.Store);
			}

			application.register('adapter:rest', EmberGraph.RESTAdapter);
			application.register('adapter:memory', EmberGraph.MemoryAdapter);
			application.register('adapter:local_storage', EmberGraph.LocalStorageAdapter);

			application.register('serializer:json', EmberGraph.JSONSerializer);
			application.register('serializer:ember_graph', EmberGraph.EmberGraphSerializer);

			application.register('type:string', EmberGraph.StringType);
			application.register('type:number', EmberGraph.NumberType);
			application.register('type:boolean', EmberGraph.BooleanType);
			application.register('type:date', EmberGraph.DateType);
			application.register('type:object', EmberGraph.ObjectType);
			application.register('type:array', EmberGraph.ArrayType);

			application.inject('controller', 'store', 'store:main');
			application.inject('route', 'store', 'store:main');
			application.inject('adapter', 'store', 'store:main');
			application.inject('serializer', 'store', 'store:main');

			if (useService) {
				application.inject('controller', 'store', 'service:store');
				application.inject('route', 'store', 'service:store');
				application.inject('adapter', 'store', 'service:store');
				application.inject('serializer', 'store', 'service:store');
			}

			if (EmberGraph.DataAdapter) {
				application.register('data-adapter:main', EmberGraph.DataAdapter);
				application.inject('data-adapter', 'store', 'store:main');
			}
		}
	});

	if (Application.instanceInitializer) {
		Application.instanceInitializer({
			name: 'ember-graph',
			initialize(instance) {
				const application = instance.lookup('application:main');
				const store = instance.lookup('store:main');
				application.set('store', store);
			}
		});
	} else {
		Application.initializer({
			name: 'ember-graph-store',
			initialize(container, application) {
				const store = container.lookup('store:main');
				application.set('store', store);
			}
		});
	}
});


<|start_filename|>test/model/relationship/polymorphism.js<|end_filename|>
(function() {
	'use strict';

	var store;

	var jsonSort = function(a, b) {
		return (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1);
	};

	module('Polymorphic Relationships Test', {
		setup: function() {
			var User = EG.Model.extend({
				posts: EG.hasMany({
					relatedType: 'post',
					inverse: 'author',
					polymorphic: true
				})
			});

			var Post = EG.Model.extend({
				author: EG.hasOne({
					relatedType: 'user',
					inverse: 'posts',
					polymorphic: true
				})
			});

			store = setupStore({
				user: User,
				admin: User.extend({
					isAdmin: EG.attr({ type: 'boolean', defaultValue: true })
				}),
				member: User.extend({
					isMember: EG.attr({ type: 'boolean', defaultValue: true })
				}),
				post: Post,
				thread: Post.extend({
					isThread: EG.attr({ type: 'boolean', defaultValue: true }),
					comments: EG.hasMany({
						relatedType: 'comment',
						inverse: 'thread'
					})
				}),
				comment: Post.extend({
					isComment: EG.attr({ type: 'boolean', defaultValue: true }),
					thread: EG.hasOne({
						relatedType: 'thread',
						inverse: 'comments'
					})
				})
			});

			store.pushPayload({
				admin: [{
					id: '1',
					posts: [
						{ type: 'thread', id: '1' },
						{ type: 'comment', id: '1' },
						{ type: 'comment', id: '3' }
					]
				}],
				member: [{
					id: '1',
					posts: [
						{ type: 'thread', id: '2' },
						{ type: 'comment', id: '2' }
					]
				}, {
					id: '2',
					posts: [
						{ type: 'comment', id: '4' }
					]
				}],
				thread: [{
					id: '1',
					author: { type: 'admin', id: '1' },
					comments: [
						{ type: 'comment', id: '1' },
						{ type: 'comment', id: '2' }
					]
				}, {
					id: '2',
					author: { type: 'member', id: '1' },
					comments: [
						{ type: 'comment', id: '3' },
						{ type: 'comment', id: '4' }
					]
				}],
				comment: [{
					id: '1',
					author: { type: 'admin', id: '1' },
					thread: { type: 'thread', id: '1' }
				}, {
					id: '2',
					author: { type: 'member', id: '1' },
					thread: { type: 'thread', id: '1' }
				}, {
					id: '3',
					author: { type: 'admin', id: '1' },
					thread: { type: 'thread', id: '2' }
				}, {
					id: '4',
					author: { type: 'member', id: '2' },
					thread: { type: 'thread', id: '2' }
				}]
			});
		}
	});

	test('Metadata is setup correctly', function() {
		expect(10);

		var userPostsMeta = store.modelFor('user').metaForRelationship('posts');
		var adminPostsMeta = store.modelFor('admin').metaForRelationship('posts');
		var memberPostsMeta = store.modelFor('member').metaForRelationship('posts');
		var postAuthorMeta = store.modelFor('post').metaForRelationship('author');
		var threadAuthorMeta = store.modelFor('thread').metaForRelationship('author');
		var commentAuthorMeta = store.modelFor('comment').metaForRelationship('author');

		deepEqual(adminPostsMeta, userPostsMeta);
		deepEqual(memberPostsMeta, userPostsMeta);
		deepEqual(threadAuthorMeta, postAuthorMeta);
		deepEqual(commentAuthorMeta, postAuthorMeta);

		ok(userPostsMeta.isPolymorphic);
		ok(postAuthorMeta.isPolymorphic);

		strictEqual(userPostsMeta.relatedType, 'post');
		strictEqual(postAuthorMeta.relatedType, 'user');

		strictEqual(userPostsMeta.inverse, 'author');
		strictEqual(postAuthorMeta.inverse, 'posts');
	});

	test('Admin values are correct', function() {
		expect(3);

		var admin = store.getRecord('admin', '1');
		var expectedPosts = [
			{ type: 'thread', id: '1' },
			{ type: 'comment', id: '1' },
			{ type: 'comment', id: '3' }
		].sort(jsonSort);

		deepEqual(admin.get('_posts').sort(jsonSort), expectedPosts);
		ok(admin.get('isAdmin'));
		ok(!admin.get('isMember'));
	});

	test('Member 1 values are correct', function() {
		expect(3);

		var member = store.getRecord('member', '1');
		var expectedPosts = [
			{ type: 'thread', id: '2' },
			{ type: 'comment', id: '2' }
		].sort(jsonSort);

		deepEqual(member.get('_posts').sort(jsonSort), expectedPosts);
		ok(!member.get('isAdmin'));
		ok(member.get('isMember'));
	});

	test('Member 2 values are correct', function() {
		expect(3);

		var member = store.getRecord('member', '2');
		var expectedPosts = [
			{ type: 'comment', id: '4' }
		];

		deepEqual(member.get('_posts'), expectedPosts);
		ok(!member.get('isAdmin'));
		ok(member.get('isMember'));
	});

	test('Thread 1 values are correct', function() {
		expect(4);

		var thread = store.getRecord('thread', '1');
		var expectedAuthor = { type: 'admin', id: '1' };
		var expectedComments = [
			{ type: 'comment', id: '1' },
			{ type: 'comment', id: '2' }
		].sort(jsonSort);

		deepEqual(thread.get('_author'), expectedAuthor);
		deepEqual(thread.get('_comments').sort(jsonSort), expectedComments);
		ok(thread.get('isThread'));
		ok(!thread.get('isComment'));
	});

	test('Thread 2 values are correct', function() {
		expect(4);

		var thread = store.getRecord('thread', '2');
		var expectedAuthor = { type: 'member', id: '1' };
		var expectedComments = [
			{ type: 'comment', id: '3' },
			{ type: 'comment', id: '4' }
		].sort(jsonSort);

		deepEqual(thread.get('_author'), expectedAuthor);
		deepEqual(thread.get('_comments').sort(jsonSort), expectedComments);
		ok(thread.get('isThread'));
		ok(!thread.get('isComment'));
	});

	test('Comment 1 values are correct', function() {
		expect(4);

		var comment = store.getRecord('comment', '1');
		deepEqual(comment.get('_author'), { type: 'admin', id: '1' });
		deepEqual(comment.get('_thread'), { type: 'thread', id: '1' });
		ok(!comment.get('isThread'));
		ok(comment.get('isComment'));
	});

	test('Comment 2 values are correct', function() {
		expect(4);

		var comment = store.getRecord('comment', '2');
		deepEqual(comment.get('_author'), { type: 'member', id: '1' });
		deepEqual(comment.get('_thread'), { type: 'thread', id: '1' });
		ok(!comment.get('isThread'));
		ok(comment.get('isComment'));
	});

	test('Comment 3 values are correct', function() {
		expect(4);

		var comment = store.getRecord('comment', '3');
		deepEqual(comment.get('_author'), { type: 'admin', id: '1' });
		deepEqual(comment.get('_thread'), { type: 'thread', id: '2' });
		ok(!comment.get('isThread'));
		ok(comment.get('isComment'));
	});

	test('Comment 4 values are correct', function() {
		expect(4);

		var comment = store.getRecord('comment', '4');
		deepEqual(comment.get('_author'), { type: 'member', id: '2' });
		deepEqual(comment.get('_thread'), { type: 'thread', id: '2' });
		ok(!comment.get('isThread'));
		ok(comment.get('isComment'));
	});
})();

<|start_filename|>src/store/store.js<|end_filename|>
import Ember from 'ember';
import RecordCache from 'ember-graph/store/record_cache';
import RecordRequestCache from 'ember-graph/store/record_request_cache';
import LookupMethods from 'ember-graph/store/lookup';
import RelationshipMethods from 'ember-graph/store/relationship';

import {
	PromiseArray,
	ModelPromiseObject
} from 'ember-graph/data/promise_object';
import { deprecateMethod } from 'ember-graph/util/util';

const Promise = Ember.RSVP.Promise;

/**
 * The store is used to manage all records in the application.
 * Ideally, there should only be one store for an application.
 *
 * @class Store
 * @constructor
 */
var Store = (Ember.Service || Ember.Object).extend({

	/**
	 * The number of milliseconds after a record in the cache expires
	 * and must be re-fetched from the server. Leave at Infinity for
	 * now, as finite timeouts will likely cause a lot of bugs.
	 *
	 * @property cacheTimeout
	 * @type Number
	 * @default Infinity
	 */
	cacheTimeout: Infinity,

	/**
	 * A boolean for whether or not to reload dirty records. If this is
	 * true, data from the server will be merged with the data on the
	 * client according to the other options defined on this class.
	 * If it's false, calling reload on a dirty record will throw an
	 * error, and any side loaded data from the server will be discarded.
	 *
	 * Note: If this is turned off, no relationship can be reloaded if
	 * either of the records is dirty. So if the server says that
	 * record 1 is connected to record 2, and you reload record 1, which
	 * is clean, Ember-Graph will abort the reload if record 2 is dirty.
	 * This is a particularly annoying corner case that can be mostly
	 * avoided in two ways: either enable reloadDirty, or ensure that
	 * records are changed and then saved or rollback back in the same
	 * 'action'. (Don't let users perform different modifications at
	 * the same time.)
	 *
	 * @property reloadDirty
	 * @for Store
	 * @type Boolean
	 * @final
	 */
	reloadDirty: true,

	/**
	 * If reloadDirty is true, this determines which side the store will
	 * settle conflicts for. If true, new client side relationships always
	 * take precedence over server side relationships loaded when the
	 * record is dirty. If false, server side relationships will overwrite
	 * any temporary client side relationships on reload.
	 *
	 * Note: This only affects relationships. Attributes aren't as tricky,
	 * so the server data can be loaded without affecting the client data.
	 * To have the server overwrite client data, use the option below.
	 *
	 * @property sideWithClientOnConflict
	 * @for Store
	 * @type Boolean
	 * @final
	 */
	sideWithClientOnConflict: true,

	/**
	 * If reloadDirty is true, this will overwrite client attributes on
	 * reload. Because of the more simplistic nature of attributes, it is
	 * recommended to keep this false. The server data will still be loaded
	 * into the record and can be activated at any time by rolling back
	 * attribute changes on the record.
	 *
	 * @property overwriteClientAttributes
	 * @for Store
	 * @type Boolean
	 * @final
	 */
	overwriteClientAttributes: false,

	/**
	 * Contains the records cached in the store. The keys are type names,
	 * and the values are nested objects keyed at the ID of the record.
	 *
	 * @property recordCache
	 * @type {RecordCache}
	 * @final
	 * @private
	 */
	recordCache: {},

	/**
	 * Contains currently pending requests for records.
	 * This allows us to chain duplicated requests.
	 *
	 * @property recordRequestCache
	 * @type {RecordRequestCache}
	 * @final
	 * @private
	 */
	recordRequestCache: {},

	initializeCaches: Ember.on('init', function() {
		this.setProperties({
			recordCache: RecordCache.create({ cacheTimeout: this.get('cacheTimeout') }),
			recordRequestCache: RecordRequestCache.create()
		});
	}),

	/**
	 * Creates a record of the specified type. The record starts in a blank
	 * state, meaning all attributes are `undefined`, all hasOne relationships
	 * are `null` and all hasMany relationships are `[]`. But before the record
	 * can be saved to the server, all relationships and attributes must be
	 * initialized (except for `serverOnly` ones). Even optional attributes
	 * and relationships must have their values filled in explicitly.
	 *
	 * @method createRecord
	 * @param {String} typeKey
	 * @param {Object} json
	 * @return {Model}
	 */
	createRecord(typeKey, json = {}) {
		const record = this.modelFor(typeKey).create(this);
		this.get('recordCache').storeRecord(record);
		record.initializeRecord(json);
		return record;
	},

	/**
	 * Returns all records of the given type that are in the cache.
	 *
	 * @method cachedRecordsFor
	 * @param {String} typeKey
	 * @return {Model[]}
	 */
	cachedRecordsFor(typeKey) {
		return this.get('recordCache').getRecords(typeKey);
	},

	/**
	 * Returns the record directly if the record is cached in the store.
	 * Otherwise returns `null`.
	 *
	 * @method getRecord
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {Model}
	 */
	getRecord(typeKey, id) {
		return this.get('recordCache').getRecord(typeKey, id);
	},

	/**
	 * Returns an array containing all of the cached records for the
	 * given type. The array will be updated as more records of the
	 * type are cached. This will contain _all_ records of that type,
	 * new and saved. This method will always return the same array
	 * for a type.
	 *
	 * @method getLiveRecordArray
	 * @param {String} typeKey
	 * @returns {Model[]}
	 */
	getLiveRecordArray(typeKey) {
		return this.get('recordCache').getLiveRecordArray(typeKey);
	},

	/**
	 * Fetches a record (or records), either from the cache or from the server.
	 * The type of `options` determines the behavior of this method:
	 *
	 * - `string` fetches a single record by ID
	 * - `string[]` fetches several records by the IDs
	 * - `object` fetches records according to the given query object
	 * - `undefined` fetches all records of the given type
	 *
	 * Any other value, including `null`, will result in an error being thrown.
     *
	 * @method find
	 * @param {String} typeKey
	 * @param {String|String[]|Object} [options]
	 * @return {PromiseObject|PromiseArray}
	 */
	find(typeKey, options) {
		switch (Ember.typeOf(options)) {
			case 'string':
				return this._findSingle(typeKey, options);
			case 'array':
				return this._findMany(typeKey, options);
			case 'object':
				return this._findQuery(typeKey, options);
			case 'undefined':
				if (arguments.length === 1) {
					return this._findAll(typeKey);
				}
				/* falls through */
			default:
				throw new Ember.Error('A bad `find` call was made to the store.');
		}
	},

	/**
	 * Gets a single record from the adapter as a PromiseObject.
	 *
	 * @param {String} typeKey
	 * @param {String} id
	 * @return {PromiseObject}
	 * @private
	 */
	_findSingle(typeKey, id) {
		let promise;

		const record = this.getRecord(typeKey, id);
		if (record) {
			promise = Promise.resolve();
		}

		const recordRequestCache = this.get('recordRequestCache');
		if (!promise) {
			const pendingRequest = recordRequestCache.getPendingRequest(typeKey, id);

			if (pendingRequest) {
				promise = pendingRequest;
			}
		}

		if (!promise) {
			promise = this.adapterFor(typeKey).findRecord(typeKey, id).then((payload) => {
				this.pushPayload(payload);
			});

			recordRequestCache.savePendingRequest(typeKey, id, promise);
		}

		return ModelPromiseObject.create({
			id, typeKey,
			promise: promise.then(() => {
				return this.getRecord(typeKey, id);
			})
		});
	},

	/**
	 * Gets many records from the adapter as a PromiseArray.
	 *
	 * @param {String} typeKey
	 * @param {String[]} ids
	 * @return {PromiseArray}
	 * @private
	 */
	_findMany(typeKey, ids = []) {
		if (ids.length === 0) {
			return PromiseArray.create({
				promise: Promise.resolve([])
			});
		}

		const idsToFetch = ids.filter((id) => {
			return (this.getRecord(typeKey, id) === null);
		});

		let promise;

		if (idsToFetch.length === 0) {
			promise = Promise.resolve();
		}

		const recordRequestCache = this.get('recordRequestCache');
		if (!promise) {
			const pendingRequest = recordRequestCache.getPendingRequest(typeKey, ids);

			if (pendingRequest) {
				promise = pendingRequest;
			}
		}

		if (!promise) {
			promise = this.adapterFor(typeKey).findMany(typeKey, ids).then((payload) => {
				this.pushPayload(payload);
			});

			recordRequestCache.savePendingRequest(typeKey, ids, promise);
		}

		return PromiseArray.create({
			promise: promise.then(() => {
				return ids.map((id) => {
					return this.getRecord(typeKey, id);
				}).toArray();
			})
		});
	},

	/**
	 * Gets all of the records of a type from the adapter as a PromiseArray.
	 *
	 * @param {String} typeKey
	 * @return {PromiseArray}
	 * @private
	 */
	_findAll(typeKey) {
		let promise;

		const recordRequestCache = this.get('recordRequestCache');
		const pendingRequest = recordRequestCache.getPendingRequest(typeKey);
		if (pendingRequest) {
			promise = pendingRequest;
		}

		if (!promise) {
			promise = this.adapterFor(typeKey).findAll(typeKey).then((payload) => {
				this.pushPayload(payload);
			});

			recordRequestCache.savePendingRequest(typeKey, promise);
		}

		return PromiseArray.create({
			promise: promise.then(() => {
				return this.cachedRecordsFor(typeKey);
			})
		});
	},

	/**
	 * Gets records for a query from the adapter as a PromiseArray.
	 *
	 * @param {String} typeKey
	 * @param {Object} query
	 * @return {PromiseArray}
	 * @private
	 */
	_findQuery(typeKey, query) {
		let promise;

		const recordRequestCache = this.get('recordRequestCache');
		const pendingRequest = recordRequestCache.getPendingRequest(typeKey, query);
		if (pendingRequest) {
			promise = pendingRequest;
		}

		if (!promise) {
			promise = this.adapterFor(typeKey).findQuery(typeKey, query);
			recordRequestCache.savePendingRequest(typeKey, query, promise);
		}

		return promise.then((payload) => {
			return {
				records: PromiseArray.create({
					promise: promise.then((payload) => {
						var records = payload.meta.matchedRecords;
						this.pushPayload(payload);

						return records.map((record) => {
							return this.getRecord(record.type, record.id);
						});
					})
				}),
				meta: payload.meta.serverMeta
			};
		});
	},

	/**
	 * Persists a record (new or old) to the server.
	 *
	 * @method saveRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the saved record
	 */
	saveRecord(record) {
		if (!record.get('isNew')) {
			return this.updateRecord(record);
		}

		if (!record.isInitialized()) {
			throw new Ember.Error('Can\'t save an uninitialized record.');
		}

		const typeKey = record.get('typeKey');

		return this.adapterFor(typeKey).createRecord(record).then((payload) => {
			const tempId = record.get('id');
			let newId = Ember.get(payload, 'meta.createdRecord.id');

			if (!newId) {
				if (payload[typeKey].length === 1) {
					newId = payload[typeKey][0].id;
				} else {
					throw new Ember.Error('Missing `createdRecord` meta attribute.');
				}
			}

			record.set('id', newId);

			const recordCache = this.get('recordCache');
			recordCache.deleteRecord(typeKey, tempId);
			recordCache.storeRecord(record);
			this.updateRelationshipsWithNewId(typeKey, tempId, newId);

			this.pushPayload(payload);
			return record;
		});
	},

	/**
	 * Saves an old record's changes to the server.
	 *
	 * @method updateRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the saved record
	 */
	updateRecord(record) {
		var recordJson = {
			id: record.get('id')
		};

		record.constructor.eachAttribute((name) => {
			recordJson[name] = record.get(name);
		});

		record.constructor.eachRelationship((name) => {
			recordJson[name] = record.get(`_${name}`);
		});

		const potentialPayload = {};
		potentialPayload[record.get('typeKey')] = [recordJson];

		return this.adapterFor(record.get('typeKey')).updateRecord(record).then((payload) => {
			this.pushPayload(payload || potentialPayload);
			return record;
		});
	},

	/**
	 * Deletes a record from the server.
	 *
	 * @method deleteRecord
	 * @param {Model} record
	 * @return {Promise}
	 */
	deleteRecord(record) {
		if (record.get('isCreating')) {
			return Promise.reject('Can\'t delete a record before it\'s created.');
		}

		const typeKey = record.get('typeKey');
		const id = record.get('id');

		if (record.get('isNew')) {
			this.deleteRecordFromStore(typeKey, id);
			return Promise.resolve();
		}

		return this.adapterFor(typeKey).deleteRecord(record).then((payload) => {
			this.deleteRecordFromStore(typeKey, id);
			this.pushPayload(payload);
		});
	},

	/**
	 * Deletes a record from the store, removing its relationships and unloading it.
	 * This should only be used when a record has already been deleted from the server.
	 *
	 * @method deleteRecordFromStore
	 * @param {String} typeKey
	 * @param {String} id
	 * @private
	 */
	deleteRecordFromStore(typeKey, id) {
		this.deleteRelationshipsForRecord(typeKey, id);
		this.get('recordCache').deleteRecord(typeKey, id);

		// TODO: #49
		// record.set('store', null);
	},

	/**
	 * Reloads a record from the server.
	 *
	 * @method reloadRecord
	 * @param {Model} record
	 * @return {Promise} Resolves to the reloaded record
	 */
	reloadRecord(record) {
		if (record.get('isDirty') && !this.get('reloadDirty')) {
			throw new Ember.Error('Can\'t reload a record while it\'s dirty and `reloadDirty` is turned off.');
		}

		return this.adapterFor(record.typeKey).findRecord(record.typeKey, record.get('id')).then((payload) => {
			this.pushPayload(payload);
			return record;
		});
	},

	/**
	 * @method extractPayload
	 * @deprecated Renamed to `pushPayload` to be more familiar to Ember-Data users.
	 */
	extractPayload: deprecateMethod('`extractPayload` is deprecated in favor of `pushPayload`', 'pushPayload'),

	/**
	 * Takes a normalized payload from the server and load the
	 * record into the store. This format is called normalized JSON
	 * and allows you to easily load multiple records in at once.
	 * Normalized JSON is a single object that contains keys that are
	 * model type names, and whose values are arrays of JSON records.
	 * In addition, there is a single `meta` key that contains some
	 * extra information that the store may need. For example, say
	 * that the following models were defined:
	 *
	 * ```js
	 * App.Post = EG.Model.extend({
	 *     title: EG.attr({ type: 'string' }),
	 *     tags: EG.hasMany({ relatedType: 'tag', inverse: null })
	 * });
	 *
	 * App.Tag = EG.Model.extend({
	 *     name: EG.attr({ type: 'string' })
	 * });
	 * ```
	 *
	 * A normalized JSON payload for these models might look like this:
	 *
	 * ```json
	 * {
	 *     "post": [
	 *         {
	 *             id: "1",
	 *             title: "Introduction To Ember-Graph",
	 *             tags: []
	 *         },
	 *         {
	 *             id: "2",
	 *             title: "Defining Models",
	 *             tags: [{ "type": "tag", "id": "1" }, { "type": "tag", "id": "3" }]
	 *         },
	 *         {
	 *             id: "3",
	 *             title: "Connecting to a REST API",
	 *             tags: [{ "type": "tag", "id": "2" }]
	 *         }
	 *     ],
	 *     "tag": [
	 *         { id: "1", name: "relationship" },
	 *         { id: "2", name: "adapter" },
	 *         { id: "3", name: "store" }
	 *     ],
	 *     "meta": {}
	 * }
	 * ```
	 *
	 * Notice that the names of the types are in singular form. Also, the
	 * records contain all attributes and relationships in the top level.
	 * In addition, all IDs (either of records or in relationships) must
	 * be strings, not numbers.
	 *
	 * This format allows records to be easily loaded into the store even
	 * if they weren't specifically requested (side-loading). The store
	 * doesn't care how or where the records come from, as long as they can
	 * be converted to this form.
	 *
	 * @method pushPayload
	 * @param {Object} payload
	 */
	pushPayload(payload = {}) {
		if (Object.keys(payload).length === 0) {
			return;
		}

		Ember.changeProperties(() => {
			const reloadDirty = this.get('reloadDirty');

			(Ember.get(payload, 'meta.serverMeta.deletedRecords') || []).forEach((record) => {
				this.deleteRecordFromStore(record.type, record.id);
			});

			delete payload.meta;

			Object.keys(payload).forEach((typeKey) => {
				const model = this.modelFor(typeKey);

				payload[typeKey].forEach((json) => {
					let record = this.getRecord(typeKey, json.id);

					if (record) {
						if (!record.get('isDirty') || reloadDirty) {
							record.loadDataFromServer(json);
						}
					} else {
						record = model.create(this);
						record.set('id', json.id);

						this.get('recordCache').storeRecord(record);
						this.connectQueuedRelationships(record);
						record.loadDataFromServer(json);
					}
				});
			});
		});
	},

	/**
	 * Unloads a record from the store. To get the record, back you must
	 * fetch it from your server again. If the record is dirty when this
	 * is called, an error will be thrown unless you set `discardChanges`
	 * to `true`.
	 *
	 * @method unloadRecord
	 * @param {Model} record
	 * @param {Boolean} [discardChanges=false]
	 */
	unloadRecord(record, discardChanges) {
		if (!discardChanges && record.get('isDirty')) {
			throw new Ember.Error('Can\'t unload a dirty record.');
		}

		Ember.changeProperties(() => {
			record.rollback();

			this.queueConnectedRelationships(record);
			this.get('recordCache').deleteRecord(record.get('typeKey'), record.get('id'));
			record.set('store', null);
		});
	}

});

Store.reopen(LookupMethods);
Store.reopen(RelationshipMethods);

export default Store;