├── .bowerrc ├── CODEOWNERS ├── src ├── main.js ├── util │ ├── array.js │ ├── copyable.js │ ├── compatibility.js │ ├── computed.js │ ├── string.js │ ├── copy.js │ ├── data_adapter.js │ ├── util.js │ ├── set.js │ └── inflector.js ├── constants.js ├── serializer │ ├── ember_graph.js │ └── serializer.js ├── attribute_type │ ├── string.js │ ├── array.js │ ├── boolean.js │ ├── type.js │ ├── date.js │ ├── number.js │ ├── object.js │ └── enum.js ├── .eslintrc ├── after_load.js ├── before_load.js ├── adapter │ ├── memory.js │ ├── local_storage.js │ ├── ember_graph │ │ ├── server.js │ │ └── adapter.js │ └── adapter.js ├── store │ ├── record_cache.js │ ├── record_request_cache.js │ └── lookup.js ├── relationship │ ├── relationship_hash.js │ ├── relationship.js │ └── relationship_store.js ├── initializer.js ├── model │ ├── states.js │ └── relationships.md ├── data │ └── promise_object.js └── shim.js ├── tasks ├── options │ ├── clean.js │ ├── connect.js │ ├── eslint.js │ ├── sass.js │ ├── yuidoc.js │ ├── qunit.js │ ├── watch.js │ ├── groundskeeper.js │ └── uglify.js ├── .eslintrc ├── setup_site_structure.js ├── build_test_runner.js ├── register_handlebars_helpers.js ├── upload_builds_to_s3.js ├── build_api_pages.js ├── transpile.js └── convert_documentation_data.js ├── site ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── stylesheets │ ├── _variables.scss │ ├── style.scss │ ├── _base.scss │ ├── _api_page.scss │ └── _highlight.scss ├── images │ └── greenlight_logo_text.png ├── index.html ├── templates │ └── api │ │ ├── content_index.hbs │ │ ├── sidebar.hbs │ │ ├── content_tabs.hbs │ │ ├── content_properties.hbs │ │ ├── shell.hbs │ │ ├── content_methods.hbs │ │ └── base.hbs └── javascripts │ └── api.js ├── .codeclimate.yml ├── .gitignore ├── test ├── .eslintrc ├── util │ ├── inflector.js │ └── initialization.js ├── model │ ├── type.js │ ├── id.js │ ├── relationship │ │ ├── creating.js │ │ ├── reconnect.js │ │ ├── reloading.js │ │ └── general.js │ ├── model.js │ └── equality.js ├── attribute_type │ ├── date.js │ ├── number.js │ ├── string.js │ ├── array.js │ ├── enum.js │ ├── boolean.js │ └── object.js ├── data │ └── promise_object.js ├── template.html.tmpl ├── configuration.js ├── relationship │ ├── relationship.js │ └── relationship_store.js └── store │ ├── record_cache.js │ ├── delete.js │ ├── unload.js │ ├── save.js │ └── record_retrieval_cache.js ├── catalog-info.yaml ├── package.json ├── LICENSE ├── bower.json ├── Gruntfile.js ├── .travis.yml └── .eslintrc /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "lib" 3 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @stingerlabs/greenlight 2 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | export * from 'ember-graph/util'; 2 | export * from 'ember-graph/model/schema'; -------------------------------------------------------------------------------- /tasks/options/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test: ['test/*.html'] 5 | }; -------------------------------------------------------------------------------- /site/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /site/stylesheets/_variables.scss: -------------------------------------------------------------------------------- 1 | $font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - "dist/*" 5 | - "src/model/relationship_load.js" 6 | -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /site/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /site/images/greenlight_logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/images/greenlight_logo_text.png -------------------------------------------------------------------------------- /tasks/options/connect.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: { 3 | options: { 4 | port: 8000, 5 | base: '.' 6 | } 7 | } 8 | }; -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /site/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stingerlabs/ember-graph/HEAD/site/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /tasks/options/eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | source: ['src/**/*.js'], 3 | tests: ['test/**/*.js'], 4 | tasks: ['Gruntfile.js', 'tasks/**/*.js'] 5 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | test/*.html 3 | .idea/* 4 | lib/* 5 | doc/* 6 | site_build/* 7 | site/stylesheets/style.css 8 | report/* 9 | temp/* 10 | -------------------------------------------------------------------------------- /tasks/options/sass.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: { 4 | 'site_build/stylesheets/style.css': 'site/stylesheets/style.scss' 5 | } 6 | } 7 | }; -------------------------------------------------------------------------------- /site/stylesheets/style.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap'; 2 | @import 'font_awesome'; 3 | @import 'highlight'; 4 | 5 | @import 'variables'; 6 | @import 'base'; 7 | @import 'api_page'; -------------------------------------------------------------------------------- /src/util/array.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var mapBy = function(property) { 4 | return this.map((item) => Ember.get(item, property)); 5 | }; 6 | 7 | export { mapBy }; -------------------------------------------------------------------------------- /tasks/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | 4 | "env": { 5 | "node": true 6 | }, 7 | 8 | "rules": { 9 | "no-console": 0, 10 | "no-use-before-define": 0 11 | } 12 | } -------------------------------------------------------------------------------- /tasks/options/yuidoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | name: 'Ember-Graph', 4 | options: { 5 | parseOnly: true, 6 | paths: ['./src'], 7 | outdir: './doc', 8 | tabtospace: 4 9 | } 10 | } 11 | }; -------------------------------------------------------------------------------- /site/stylesheets/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | //font-family: $font-family; 3 | } 4 | 5 | footer { 6 | hr { 7 | margin-bottom: .5em; 8 | } 9 | 10 | margin-left: 5em; 11 | margin-right: 5em; 12 | margin-bottom: .5em; 13 | } -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export var RelationshipTypes = { 2 | HAS_ONE_KEY: 'hasOne', 3 | HAS_MANY_KEY: 'hasMany' 4 | }; 5 | 6 | export var RelationshipStates = { 7 | CLIENT_STATE: 'client', 8 | SERVER_STATE: 'server', 9 | DELETED_STATE: 'deleted' 10 | }; -------------------------------------------------------------------------------- /src/serializer/ember_graph.js: -------------------------------------------------------------------------------- 1 | import JSONSerializer from 'ember-graph/serializer/json'; 2 | 3 | /** 4 | * @class EmberGraphSerializer 5 | * @extends JSONSerializer 6 | */ 7 | export default JSONSerializer.extend({ 8 | polymorphicRelationships: true 9 | }); -------------------------------------------------------------------------------- /tasks/options/qunit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | options: { 5 | coverage: { 6 | src: ['dist/ember-graph.js'], 7 | instrumentedFiles: 'temp/', 8 | lcovReport: 'report/' 9 | } 10 | }, 11 | 12 | all: ['test/*.html'] 13 | }; -------------------------------------------------------------------------------- /tasks/options/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | nospawn: true 4 | }, 5 | code: { 6 | files: ['src/**/*.js'], 7 | tasks: ['transpile'] 8 | }, 9 | test: { 10 | files: ['test/**/*.js'], 11 | tasks: ['build_test_runner'] 12 | } 13 | }; -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | 4 | "env": { 5 | "browser": true, 6 | "qunit": true 7 | }, 8 | 9 | "globals": { 10 | "EG": false, 11 | "EmberGraph": false, 12 | "Em": false, 13 | "Ember": false, 14 | "setupStore": true 15 | } 16 | } -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /site/templates/api/content_index.hbs: -------------------------------------------------------------------------------- 1 | {{#if properties}} 2 |

Properties

3 | 4 | 11 | {{/if}} 12 | 13 | {{#if methods}} 14 |

Methods

15 | 16 | 23 | {{/if}} -------------------------------------------------------------------------------- /tasks/setup_site_structure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var execSync = require('child_process').execSync; 4 | 5 | module.exports = function(grunt) { 6 | grunt.registerTask('setup_site_structure', function() { 7 | execSync('rm -rf site_build'); 8 | execSync('mkdir -p site_build/api'); 9 | 10 | execSync('cp -r site/fonts site_build/'); 11 | execSync('cp -r site/javascripts site_build/'); 12 | execSync('cp -r site/index.html site_build/'); 13 | 14 | execSync('mkdir -p site_build/stylesheets'); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /tasks/options/groundskeeper.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | files: { 4 | 'dist/ember-graph.prod.js': 'dist/ember-graph.js' 5 | }, 6 | 7 | options: { 8 | console: false, 9 | 'debugger': true, 10 | namespace: [ 11 | 'Em.assert', 'Ember.assert', 12 | 'Em.warn', 'Ember.warn', 13 | 'Em.runInDebug', 'Ember.runInDebug', 14 | 'Em.deprecate', 'Ember.deprecate', 15 | // TODO: Clean up before transpiling 16 | '_ember.default.assert', 17 | '_ember.default.warn', 18 | '_ember.default.runInDebug', 19 | '_ember.default.deprecate' 20 | ] 21 | } 22 | } 23 | }; -------------------------------------------------------------------------------- /site/templates/api/sidebar.hbs: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: ember-graph 5 | description: A data persistence library for Ember.js with a focus on complex object graphs 6 | annotations: 7 | github.com/project-slug: stingerlabs/ember-graph 8 | sonarqube.org/project-key: stingerlabs_ember-graph 9 | snyk.io/org-id: de43b40c-8694-45ec-985c-404d8ab5977e 10 | links: 11 | - url: https://greenlightguru.slack.com/archives/C05FS321Y6R 12 | title: '#ask channel' 13 | icon: help 14 | tags: 15 | - javascript 16 | - ember 17 | spec: 18 | type: library 19 | lifecycle: production 20 | -------------------------------------------------------------------------------- /test/util/inflector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | module('Inflector Test'); 5 | 6 | test('Can override singular rules', 2, function() { 7 | strictEqual(EmberGraph.Inflector.pluralize('word'), 'words'); 8 | EmberGraph.Inflector.overridePluralRule('word', 'foobar'); 9 | strictEqual(EmberGraph.Inflector.pluralize('word'), 'foobar'); 10 | }); 11 | 12 | test('Can override plural rules', 2, function() { 13 | strictEqual(EmberGraph.Inflector.singularize('words'), 'word'); 14 | EmberGraph.Inflector.overrideSingularRule('word', 'foobar'); 15 | strictEqual(EmberGraph.Inflector.singularize('word'), 'foobar'); 16 | }); 17 | })(); 18 | -------------------------------------------------------------------------------- /site/templates/api/content_tabs.hbs: -------------------------------------------------------------------------------- 1 | {{#if index}} 2 |
3 | 14 | 15 |
16 |
17 | {{{index}}} 18 |
19 |
20 | {{{properties}}} 21 |
22 |
23 | {{{methods}}} 24 |
25 |
26 |
27 | {{/if}} -------------------------------------------------------------------------------- /test/model/type.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store = setupStore({ test: EG.Model.extend() }); 5 | 6 | module('Model Type Property Test'); 7 | 8 | test('typeKey exists on instances', function() { 9 | expect(1); 10 | 11 | var model = store.createRecord('test', {}); 12 | 13 | ok(model.typeKey === 'test'); 14 | }); 15 | 16 | test('typeKey exists on the class', function() { 17 | expect(1); 18 | 19 | var TestModel = store.modelFor('test'); 20 | 21 | ok(TestModel.typeKey === 'test'); 22 | }); 23 | 24 | test('Looking up a type from the store works', function() { 25 | expect(1); 26 | 27 | ok(EG.Model.detect(store.modelFor('test'))); 28 | }); 29 | })(); 30 | -------------------------------------------------------------------------------- /src/util/copyable.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const Mixin = Ember.Mixin; 4 | 5 | /** 6 | Implements some standard methods for copying an object. Add this mixin to 7 | any object you create that can create a copy of itself. This mixin is 8 | added automatically to the built-in array. 9 | You should generally implement the `copy()` method to return a copy of the 10 | receiver. 11 | @class Copyable 12 | */ 13 | export default Mixin.create({ 14 | /** 15 | __Required.__ You must implement this method to apply this mixin. 16 | Override to return a copy of the receiver. Default implementation raises 17 | an exception. 18 | @method copy 19 | @param {Boolean} deep if `true`, a deep copy of the object should be made 20 | @return {Object} copy of receiver 21 | */ 22 | copy: null 23 | }); 24 | -------------------------------------------------------------------------------- /src/attribute_type/string.js: -------------------------------------------------------------------------------- 1 | import AttributeType from 'ember-graph/attribute_type/type'; 2 | 3 | /** 4 | * @class StringType 5 | * @extends AttributeType 6 | * @constructor 7 | */ 8 | export default AttributeType.extend({ 9 | 10 | /** 11 | * Coerces the given value to a string, unless it's `null`, 12 | * in which case it returns `null`. 13 | * 14 | * @method serialize 15 | * @param {String} str 16 | * @returns {String} 17 | */ 18 | serialize: function(str) { 19 | return (str === null || str === undefined ? null : '' + str); 20 | }, 21 | 22 | /** 23 | * Coerces the given value to a string, unless it's `null`, 24 | * in which case it returns `null`. 25 | * 26 | * @method deserialize 27 | * @param {String} json 28 | * @returns {String} 29 | */ 30 | deserialize: function(json) { 31 | return (json === null || json === undefined ? null : '' + json); 32 | } 33 | }); -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | 4 | "ecmaFeatures": { 5 | "arrowFunctions": true, 6 | "blockBindings": true, 7 | "defaultParams": true, 8 | "destructuring": true, 9 | "modules": true, 10 | "objectLiteralComputedProperties": true, 11 | "objectLiteralDuplicateProperties": true, 12 | "objectLiteralShorthandMethods": true, 13 | "objectLiteralShorthandProperties": true, 14 | "restParams": true, 15 | "spread": true, 16 | "templateStrings": true 17 | }, 18 | 19 | "env": { 20 | "browser": true, 21 | "es6": true 22 | }, 23 | 24 | "rules": { 25 | "arrow-parens": [2, "always"], 26 | "arrow-spacing": [2, { "before": true, "after": true }], 27 | "no-const-assign": 2 28 | // To be enabled over time 29 | //"no-var": 2, 30 | //"object-shorthand": 2, 31 | //"prefer-arrow-callback": 2, 32 | //"prefer-const": 2, 33 | //"prefer-template": 2 34 | } 35 | } -------------------------------------------------------------------------------- /tasks/options/uglify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | module.exports = { 3 | options: { 4 | mangle: true, 5 | compress: { 6 | global_defs: { 7 | DEBUG: false, 8 | RELEASE: true 9 | }, 10 | sequences: true, 11 | properties: true, 12 | drop_debugger: true, 13 | unsafe: false, 14 | conditionals: true, 15 | comparisons: true, 16 | evaluate: true, 17 | booleans: true, 18 | dead_code: true, 19 | loops: true, 20 | unused: true, 21 | hoist_funs: false, 22 | hoist_vars: false, 23 | if_return: true, 24 | join_vars: true, 25 | cascade: true, 26 | warnings: true, 27 | negate_iife: false, 28 | pure_getters: false, 29 | pure_funcs: null, 30 | drop_console: true 31 | }, 32 | screw_ie8: true 33 | }, 34 | 35 | release: { 36 | files: { 37 | 'dist/ember-graph.min.js': 'dist/ember-graph.prod.js' 38 | } 39 | } 40 | }; 41 | /* eslint-enable camelcase */ -------------------------------------------------------------------------------- /test/attribute_type/date.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.DateType.create(); 5 | 6 | module('Date Attribute Type Test'); 7 | 8 | test('Serialization works correctly', function() { 9 | expect(4); 10 | 11 | var now = new Date(); 12 | strictEqual(type.serialize(now), now.getTime()); 13 | strictEqual(type.serialize(), null); 14 | strictEqual(type.serialize(null), null); 15 | strictEqual(type.serialize(832748734), 832748734); 16 | }); 17 | 18 | test('Deserialization works correctly', function() { 19 | expect(6); 20 | 21 | strictEqual(type.deserialize(undefined), null); 22 | strictEqual(type.deserialize(null), null); 23 | strictEqual(type.deserialize({}), null); 24 | strictEqual(type.deserialize([]), null); 25 | ok(type.isEqual(type.deserialize('2012-01-01'), new Date('2012-01-01'))); 26 | ok(type.isEqual(type.deserialize(12343466000), new Date(12343466000))); 27 | }); 28 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-graph", 3 | "license": "MIT", 4 | "version": "1.1.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/stingerlabs/ember-graph" 8 | }, 9 | "engines": { 10 | "node": ">= 0.12.0" 11 | }, 12 | "devDependencies": { 13 | "adm-zip": "0.4.4", 14 | "aws-sdk": "2.0.8", 15 | "babel": "5.0.8", 16 | "fs-readdir-recursive": "0.1.1", 17 | "grunt": "0.4.5", 18 | "grunt-contrib-clean": "0.5.0", 19 | "grunt-contrib-connect": "0.6.0", 20 | "grunt-contrib-uglify": "0.3.2", 21 | "grunt-contrib-watch": "0.5.3", 22 | "grunt-contrib-yuidoc": "0.5.2", 23 | "grunt-eslint": "17.1.0", 24 | "grunt-groundskeeper": "0.1.7", 25 | "grunt-qunit-istanbul": "0.4.5", 26 | "grunt-sass": "0.18.1", 27 | "handlebars": "2.0.0-alpha.4", 28 | "highlight.js": "8.0.0", 29 | "marked": "0.3.2", 30 | "node-sass": "2.1.1" 31 | }, 32 | "peerDependencies": { 33 | "eslint": "1.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/util/compatibility.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /** 4 | * This function will return `true` if the current version of 5 | * Ember is at least the version number specified. If not, 6 | * it will return false. 7 | * 8 | * @param {Number} major 9 | * @param {Number} minor 10 | * @param {Number} patch 11 | * @return {Boolean} 12 | */ 13 | function verifyAtLeastEmberVersion(major, minor, patch) { 14 | const emberVersionParts = Ember.VERSION.split(/\.|\-/); 15 | const emberVersionNumbers = emberVersionParts.map((part) => parseInt(part, 10)); 16 | 17 | if (emberVersionNumbers[0] < major) { 18 | return false; 19 | } else if (emberVersionNumbers[0] > major) { 20 | return true; 21 | } 22 | 23 | if (emberVersionNumbers[1] < minor) { 24 | return false; 25 | } else if (emberVersionNumbers[1] > minor) { 26 | return true; 27 | } 28 | 29 | if (emberVersionNumbers[2] < patch) { 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | export { verifyAtLeastEmberVersion }; -------------------------------------------------------------------------------- /site/javascripts/api.js: -------------------------------------------------------------------------------- 1 | $('.api-index .nav-tabs a').click(function(e) { 2 | e.preventDefault(); 3 | $(this).tab('show'); 4 | window.location.hash = $(this).attr('href'); 5 | }); 6 | 7 | function showPane(href) { 8 | $('.api-index .nav-tabs a[href="' + href + '"]').tab('show'); 9 | } 10 | 11 | $(window).on('hashchange', function() { 12 | var hash = window.location.hash; 13 | var el = $(hash); 14 | 15 | if (el.length > 0 && !el.is(':visible')) { 16 | var href = null; 17 | 18 | if (el.hasClass('tab-pane')) { 19 | href = '#' + el.attr('id'); 20 | } else { 21 | href = '#' + el.parents('.tab-pane').attr('id'); 22 | } 23 | 24 | showPane(href); 25 | $('html, body').scrollTop(el.offset().top); 26 | } else if (el.length === 0) { 27 | window.location.hash = '#index'; 28 | showPane('#index'); 29 | } 30 | 31 | // Make sure the index tab is selected if the panel is visible 32 | if ($('#index').is(':visible')) { 33 | showPane('#index'); 34 | } 35 | }); 36 | 37 | $(window).trigger('hashchange'); -------------------------------------------------------------------------------- /site/templates/api/content_properties.hbs: -------------------------------------------------------------------------------- 1 | {{#each properties}} 2 | {{#if @index}} 3 |
4 | {{/if}} 5 | 6 |
7 |

8 | {{name}} 9 | 10 | {{{type}}} 11 | {{#if static}} 12 | static 13 | {{/if}} 14 | {{#if readOnly}} 15 | read only 16 | {{/if}} 17 | {{#if private}} 18 | private 19 | {{/if}} 20 | {{#if protected}} 21 | protected 22 | {{/if}} 23 | {{#if deprecated}} 24 | deprecated 25 | {{/if}} 26 | 27 |

28 | Defined in: 29 | 30 | {{file.path}}:{{file.line}} 31 | 32 | 33 |

{{{description}}}

34 | 35 | {{#if default}} 36 | 37 | Default: {{{default}}} 38 | 39 | {{/if}} 40 |
41 | {{/each}} -------------------------------------------------------------------------------- /test/attribute_type/number.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.NumberType.create(); 5 | 6 | module('Number Attribute Type Test'); 7 | 8 | test('Serialization works correctly', function() { 9 | expect(7); 10 | 11 | strictEqual(500, type.serialize(500)); 12 | strictEqual(5, type.serialize(new Number(5))); // eslint-disable-line no-new-wrappers 13 | strictEqual(0, type.serialize(null)); 14 | strictEqual(0, type.serialize()); 15 | strictEqual(0, type.serialize({})); 16 | strictEqual(4, type.serialize('4')); 17 | strictEqual(0, type.serialize(true)); 18 | }); 19 | 20 | test('Deserialization works correctly', function() { 21 | expect(8); 22 | 23 | strictEqual(type.deserialize(undefined), 0); 24 | strictEqual(type.deserialize(null), 0); 25 | strictEqual(type.deserialize(42), 42); 26 | strictEqual(type.deserialize(false), 0); 27 | strictEqual(type.deserialize(true), 0); 28 | strictEqual(type.deserialize('500'), 500); 29 | strictEqual(type.deserialize({}), 0); 30 | strictEqual(type.deserialize([]), 0); 31 | }); 32 | })(); -------------------------------------------------------------------------------- /src/after_load.js: -------------------------------------------------------------------------------- 1 | /* global define require */ 2 | 3 | var configureAliases = function() { 4 | var configureAlias = function(original, alias) { 5 | define(alias, ['exports', original], function(exports, original) { 6 | for (var key in original) { 7 | if (original.hasOwnProperty(key)) { 8 | exports[key] = original[key]; 9 | } 10 | } 11 | }); 12 | }; 13 | 14 | configureAlias('ember-graph/main', 'ember-graph'); 15 | configureAlias('ember-graph/util/util', 'ember-graph/util'); 16 | configureAlias('ember-graph/serializer/serializer', 'ember-graph/serializer'); 17 | configureAlias('ember-graph/adapter/adapter', 'ember-graph/adapter'); 18 | configureAlias('ember-graph/adapter/ember_graph/adapter', 'ember-graph/adapter/ember_graph'); 19 | configureAlias('ember-graph/model/model', 'ember-graph/model'); 20 | configureAlias('ember-graph/attribute_type/type', 'ember-graph/attribute_type'); 21 | configureAlias('ember-graph/store/store', 'ember-graph/store'); 22 | }; 23 | 24 | require(['ember-graph/initializer']); 25 | require(['ember-graph/shim']); 26 | 27 | configureAliases(); -------------------------------------------------------------------------------- /test/attribute_type/string.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.StringType.create(); 5 | 6 | module('String Attribute Type Test'); 7 | 8 | test('Serialization works correctly', function() { 9 | expect(7); 10 | 11 | strictEqual('foo', type.serialize('foo')); 12 | strictEqual('bar', type.serialize(new String('bar'))); // eslint-disable-line no-new-wrappers 13 | strictEqual(null, type.serialize(null)); 14 | strictEqual(null, type.serialize()); 15 | strictEqual({}.toString(), type.serialize({})); 16 | strictEqual('4', type.serialize(4)); 17 | strictEqual('true', type.serialize(true)); 18 | }); 19 | 20 | test('Deserialization works correctly', function() { 21 | expect(7); 22 | 23 | strictEqual(null, type.deserialize(undefined)); 24 | strictEqual(null, type.deserialize(null)); 25 | strictEqual('42', type.deserialize(42)); 26 | strictEqual('false', type.deserialize(false)); 27 | strictEqual('500', type.deserialize('500')); 28 | strictEqual({}.toString(), type.deserialize({})); 29 | strictEqual([].toString(), type.deserialize([])); 30 | }); 31 | })(); -------------------------------------------------------------------------------- /test/attribute_type/array.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | var type = EG.ArrayType.create(); 6 | 7 | module('Array Attribute Type Test', { 8 | setup: function() { 9 | store = setupStore({ 10 | lotteryTicket: EG.Model.extend({ 11 | numbers: EG.attr('array') 12 | }) 13 | }); 14 | 15 | store.pushPayload({ 16 | lotteryTicket: [ 17 | { 18 | id: '1', 19 | numbers: [4, 8, 15, 16, 23, 42] 20 | } 21 | ] 22 | }); 23 | } 24 | }); 25 | 26 | test('Comparison works correctly', function() { 27 | expect(5); 28 | 29 | ok(type.isEqual([], [])); 30 | ok(type.isEqual(['hello'], ['hello'])); 31 | ok(type.isEqual([1, 2, 3], [1, 2, 3])); 32 | ok(!type.isEqual([1, 2], [2, 1])); 33 | ok(!type.isEqual([''], [null])); 34 | }); 35 | 36 | test('Changes can be observed', 2, function() { 37 | var ticket = store.getRecord('lotteryTicket', '1'); 38 | 39 | ticket.addObserver('numbers.[]', function() { 40 | ok(true); 41 | }); 42 | 43 | ticket.get('numbers').pushObject(0); 44 | ticket.set('numbers', ticket.get('numbers').slice(0, 6)); 45 | }); 46 | })(); -------------------------------------------------------------------------------- /test/model/id.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store = setupStore({ test: EG.Model.extend() }); 5 | 6 | module('Model ID Test'); 7 | 8 | test('Existing ID loads correctly', function() { 9 | expect(1); 10 | 11 | store.pushPayload({ 12 | test: [{ id: 'TEST_ID' }] 13 | }); 14 | 15 | strictEqual(store.getRecord('test', 'TEST_ID').get('id'), 'TEST_ID'); 16 | }); 17 | 18 | test('New ID is created', function() { 19 | expect(1); 20 | 21 | var model = store.createRecord('test', {}); 22 | 23 | ok(EG.String.startsWith(model.get('id'), EG.Model.temporaryIdPrefix)); 24 | }); 25 | 26 | test('A permanent ID cannot be changed', function() { 27 | expect(1); 28 | 29 | store.pushPayload({ 30 | test: [{ id: '1' }] 31 | }); 32 | 33 | var model = store.getRecord('test', '1'); 34 | 35 | throws(function() { 36 | model.set('id', ''); 37 | }); 38 | }); 39 | 40 | test('A temporary ID can be changed to a permanent one', function() { 41 | expect(1); 42 | 43 | var model = store.createRecord('test', {}); 44 | model.set('id', ''); 45 | 46 | ok(model.get('id') === ''); 47 | }); 48 | })(); 49 | 50 | -------------------------------------------------------------------------------- /test/attribute_type/enum.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.EnumType.extend({ 5 | defaultValue: 'ORANGE', 6 | values: ['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'INDIGO', 'VIOLET'] 7 | }).create(); 8 | 9 | module('Enum Attribute Type Test'); 10 | 11 | test('Values are serialized correctly', function() { 12 | expect(7); 13 | 14 | strictEqual(type.serialize('BLUE'), 'BLUE'); 15 | strictEqual(type.serialize('RED'), 'RED'); 16 | strictEqual(type.serialize('green'), 'green'); 17 | strictEqual(type.serialize(''), 'ORANGE'); 18 | strictEqual(type.serialize(null), 'ORANGE'); 19 | strictEqual(type.serialize(new String('INDIGO')), 'INDIGO'); // eslint-disable-line no-new-wrappers 20 | strictEqual(type.serialize(String('violet')), 'violet'); 21 | }); 22 | 23 | test('Values are compared correctly', function() { 24 | expect(7); 25 | 26 | ok(type.isEqual('RED', 'RED')); 27 | ok(type.isEqual('orange', 'orange')); 28 | ok(type.isEqual('yellow', 'YELLOW')); 29 | ok(type.isEqual(String('Green'), 'green')); 30 | ok(!type.isEqual('BLUE', 'RED')); 31 | ok(!type.isEqual(null, 'RED')); 32 | ok(!type.isEqual()); 33 | }); 34 | })(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Gordon Kristan, greenlight.guru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-graph", 3 | "description": "Ember persistence library for complex object graphs", 4 | "version": "1.1.1", 5 | "main": "dist/ember-graph.js", 6 | "homepage": "https://github.com/stingerlabs/ember-graph", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com/stingerlabs/ember-graph.git" 10 | }, 11 | "dependencies": { 12 | "ember": ">=1.8.0" 13 | }, 14 | "devDependencies": { 15 | "qunit": "1.15", 16 | "ember-1.8.0": "ember#1.8.0", 17 | "ember-1.13.0": "ember#1.13.0", 18 | "ember-2.0.0": "ember#2.0.0" 19 | }, 20 | "keywords": [ 21 | "ember", 22 | "store", 23 | "graph", 24 | "persistence" 25 | ], 26 | "authors": [ 27 | { "name": "Gordon Kristan" }, 28 | { "name": "greenlight.guru", "homepage": "http://www.greenlight.guru/" } 29 | ], 30 | "license": "MIT", 31 | "ignore": [ 32 | "lib", 33 | "node_modules", 34 | "report", 35 | "site", 36 | "tasks", 37 | "test", 38 | ".bowerrc", 39 | ".codeclimate.yml", 40 | ".eslintrc", 41 | ".gitignore", 42 | ".travis.yml", 43 | "Gruntfile.js" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /test/data/promise_object.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Promise Object Tests', { 7 | setup: function() { 8 | store = setupStore({ 9 | tag: EG.Model.extend({ 10 | name: EG.attr({ 11 | type: 'string' 12 | }) 13 | }) 14 | }); 15 | 16 | store.pushPayload({ 17 | tag: [ 18 | { id: '1', name: 'tag1' }, 19 | { id: '2', name: 'tag2' }, 20 | { id: '3', name: 'tag3' }, 21 | { id: '4', name: 'tag4' } 22 | ] 23 | }); 24 | } 25 | }); 26 | 27 | asyncTest('Model promise object correctly reports the ID and typeKey', function() { 28 | expect(6); 29 | 30 | var resolve = null; 31 | var modelPromise = EG.ModelPromiseObject.create({ 32 | id: '1', 33 | typeKey: 'tag', 34 | promise: new Em.RSVP.Promise(function(r) { 35 | resolve = r; 36 | }) 37 | }); 38 | 39 | start(); 40 | strictEqual(modelPromise.get('id'), '1'); 41 | strictEqual(modelPromise.get('typeKey'), 'tag'); 42 | strictEqual(modelPromise.get('name'), undefined); 43 | stop(); 44 | 45 | resolve(store.getRecord('tag', '1')); 46 | 47 | modelPromise.then(function() { 48 | start(); 49 | strictEqual(modelPromise.get('id'), '1'); 50 | strictEqual(modelPromise.get('typeKey'), 'tag'); 51 | strictEqual(modelPromise.get('name'), 'tag1'); 52 | }); 53 | }); 54 | })(); 55 | 56 | -------------------------------------------------------------------------------- /test/util/initialization.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var App; 5 | 6 | module('Initializer Test', { 7 | teardown: function() { 8 | App.destroy(); 9 | } 10 | }); 11 | 12 | asyncTest('Store is on application instance', 1, function() { 13 | Ember.onLoad('Ember.Application', function(Application) { 14 | App = Application.create({ 15 | rootElement: '#test-app', 16 | ready: function() { 17 | start(); 18 | ok(App.store instanceof EmberGraph.Store); 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | asyncTest('Store is available as store:main', 1, function() { 25 | Ember.onLoad('Ember.Application', function(Application) { 26 | App = Application.create({ 27 | rootElement: '#test-app', 28 | ready: function() { 29 | start(); 30 | 31 | var container = this.__container__; 32 | ok(container.lookup('store:main') instanceof EmberGraph.Store); 33 | } 34 | }); 35 | }); 36 | }); 37 | 38 | if (Ember.Service) { 39 | asyncTest('Store is available as service:store', 1, function() { 40 | Ember.onLoad('Ember.Application', function(Application) { 41 | App = Application.create({ 42 | rootElement: '#test-app', 43 | ready: function() { 44 | start(); 45 | 46 | var container = this.__container__; 47 | ok(container.lookup('service:store') instanceof EmberGraph.Store); 48 | } 49 | }); 50 | }); 51 | }); 52 | } 53 | })(); 54 | -------------------------------------------------------------------------------- /src/attribute_type/array.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AttributeType from 'ember-graph/attribute_type/type'; 3 | 4 | /** 5 | * @class ArrayType 6 | * @extends AttributeType 7 | * @constructor 8 | */ 9 | export default AttributeType.extend({ 10 | 11 | /** 12 | * If the object is an array, it's returned. Otherwise, `null` is returned. 13 | * This doesn't check the individual elements, just the array. 14 | * 15 | * @method serialize 16 | * @param {Array} arr 17 | * @returns {Array} 18 | */ 19 | serialize: function(arr) { 20 | if (Ember.isNone(arr)) { 21 | return null; 22 | } 23 | 24 | return (Ember.isArray(arr.toArray ? arr.toArray() : arr) ? arr : null); 25 | }, 26 | 27 | /** 28 | * If the object is an array, it's returned. Otherwise, `null` is returned. 29 | * This doesn't check the individual elements, just the array. 30 | * 31 | * @method deserialize 32 | * @param {Array} arr 33 | * @returns {Array} 34 | */ 35 | deserialize: function(arr) { 36 | return (Ember.isArray(arr) ? arr : null); 37 | }, 38 | 39 | /** 40 | * Compares two arrays using `Ember.compare`. 41 | * 42 | * @method isEqual 43 | * @param {Array} a 44 | * @param {Array} b 45 | * @returns {Boolean} 46 | */ 47 | isEqual: function(a, b) { 48 | if (!Ember.isArray(a) || !Ember.isArray(b)) { 49 | return false; 50 | } 51 | 52 | return Ember.compare(a.toArray(), b.toArray()) === 0; 53 | } 54 | }); -------------------------------------------------------------------------------- /src/util/computed.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { verifyAtLeastEmberVersion } from 'ember-graph/util/compatibility'; 4 | 5 | const isNewVersion = verifyAtLeastEmberVersion(1, 12, 0); 6 | 7 | const oldComputed = (...args) => { 8 | const dependentProperties = args.slice(0, -1); 9 | const definition = args[args.length - 1]; 10 | const readOnly = !definition.set; 11 | 12 | if (readOnly) { 13 | return Ember.computed(...dependentProperties, function(key) { 14 | return definition.get.call(this, key); 15 | }).readOnly(); 16 | } else { 17 | return Ember.computed(...dependentProperties, function(key, value) { 18 | if (arguments.length > 1) { 19 | definition.set.call(this, key, value); 20 | } 21 | 22 | return definition.get.call(this, key); 23 | }); 24 | } 25 | }; 26 | 27 | const newComputed = (...args) => { 28 | const dependentProperties = args.slice(0, -1); 29 | const definition = args[args.length - 1]; 30 | const readOnly = !definition.set; 31 | 32 | if (definition.set) { 33 | const oldSet = definition.set; 34 | definition.set = function(key, value) { 35 | oldSet.call(this, key, value); 36 | return definition.get.call(this, key); 37 | }; 38 | } 39 | 40 | const property = Ember.computed(...dependentProperties, definition); 41 | 42 | if (readOnly) { 43 | return property.readOnly(); 44 | } else { 45 | return property; 46 | } 47 | }; 48 | 49 | const computed = (isNewVersion ? newComputed : oldComputed); 50 | 51 | export { computed }; -------------------------------------------------------------------------------- /test/attribute_type/boolean.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.BooleanType.create(); 5 | 6 | module('Boolean Attribute Type Test'); 7 | 8 | test('Serialization works correctly', function() { 9 | expect(12); 10 | 11 | strictEqual(type.serialize(true), true); 12 | strictEqual(type.serialize('true'), true); 13 | strictEqual(type.serialize(new Boolean(true)), true); // eslint-disable-line no-new-wrappers 14 | strictEqual(type.serialize(Boolean(true)), true); 15 | 16 | strictEqual(type.serialize(false), false); 17 | strictEqual(type.serialize('false'), false); 18 | strictEqual(type.serialize(new Boolean(false)), false); // eslint-disable-line no-new-wrappers 19 | strictEqual(type.serialize(Boolean(false)), false); 20 | 21 | strictEqual(type.serialize(''), false); 22 | strictEqual(type.serialize(), false); 23 | strictEqual(type.serialize(null), false); 24 | strictEqual(type.serialize(5), false); 25 | }); 26 | 27 | test('Deserialization works correctly', function() { 28 | expect(9); 29 | 30 | strictEqual(type.deserialize(undefined), false); 31 | strictEqual(type.deserialize(null), false); 32 | strictEqual(type.deserialize(42), false); 33 | strictEqual(type.deserialize(false), false); 34 | strictEqual(type.deserialize(true), true); 35 | strictEqual(type.deserialize('true'), true); 36 | strictEqual(type.deserialize('true_'), false); 37 | strictEqual(type.deserialize({}), false); 38 | strictEqual(type.deserialize([]), false); 39 | }); 40 | })(); -------------------------------------------------------------------------------- /src/attribute_type/boolean.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AttributeType from 'ember-graph/attribute_type/type'; 3 | 4 | /** 5 | * @class BooleanType 6 | * @extends AttributeType 7 | * @constructor 8 | */ 9 | export default AttributeType.extend({ 10 | 11 | /** 12 | * @property defaultValue 13 | * @default false 14 | * @final 15 | */ 16 | defaultValue: false, 17 | 18 | /** 19 | * Coerces to a boolean using 20 | * {{link-to-method 'BooleanType' 'coerceToBoolean'}}. 21 | * 22 | * @method serialize 23 | * @param {Boolean} bool 24 | * @return {Boolean} 25 | */ 26 | serialize: function(bool) { 27 | return this.coerceToBoolean(bool); 28 | }, 29 | 30 | /** 31 | * Coerces to a boolean using 32 | * {{link-to-method 'BooleanType' 'coerceToBoolean'}}. 33 | * 34 | * @method deserialize 35 | * @param {Boolean} json 36 | * @return {Boolean} 37 | */ 38 | deserialize: function(json) { 39 | return this.coerceToBoolean(json); 40 | }, 41 | 42 | /** 43 | * Coerces a value to a boolean. `true` and `'true'` resolve to 44 | * `true`, everything else resolves to `false`. 45 | * 46 | * @method coerceToBoolean 47 | * @param {Any} obj 48 | * @return {Boolean} 49 | */ 50 | coerceToBoolean: function(obj) { 51 | if (Ember.typeOf(obj) === 'boolean' && obj == true) { // eslint-disable-line eqeqeq 52 | return true; 53 | } 54 | 55 | if (Ember.typeOf(obj) === 'string' && obj == 'true') { // eslint-disable-line eqeqeq 56 | return true; 57 | } 58 | 59 | return false; 60 | } 61 | }); -------------------------------------------------------------------------------- /site/templates/api/shell.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{{sidebar}}} 5 |
6 |
7 | {{#if class}} 8 | {{#with class}} 9 |
10 |

11 | {{name}} Class 12 |

13 |
14 |
15 | 16 | 36 |
37 |

{{{description}}}

38 | {{/with}} 39 | {{/if}} 40 | 41 | {{#if namespace}} 42 | {{#with namespace}} 43 |
44 |

45 | {{name}} Namespace 46 |

47 |
48 |
49 | 50 | 51 |
52 |

{{{description}}}

53 | {{/with}} 54 | {{/if}} 55 | 56 | {{#if content}} 57 | {{{content}}} 58 | {{/if}} 59 |
60 |
61 |
-------------------------------------------------------------------------------- /src/attribute_type/type.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /** 4 | * Specifies the details of a custom attribute type. 5 | * Comes with reasonable defaults that can be used for some extended types. 6 | * 7 | * @class AttributeType 8 | * @constructor 9 | */ 10 | export default Ember.Object.extend({ 11 | 12 | /** 13 | * The default value to use if a value of this type is missing. 14 | * Can be overridden in subclasses. 15 | * 16 | * @property defaultValue 17 | * @type Any 18 | * @default null 19 | * @final 20 | */ 21 | defaultValue: null, 22 | 23 | /** 24 | * Converts a value of this type to its JSON form. 25 | * The default function returns the value given. 26 | * 27 | * @method serialize 28 | * @param {Any} obj Javascript value 29 | * @return {JSON} JSON representation 30 | */ 31 | serialize: function(obj) { 32 | return obj; 33 | }, 34 | 35 | /** 36 | * Converts a JSON value to its Javascript form. 37 | * The default function returns the value given. 38 | * 39 | * @method deserialize 40 | * @param {JSON} json JSON representation of object 41 | * @return {Any} Javascript value 42 | */ 43 | deserialize: function(json) { 44 | return json; 45 | }, 46 | 47 | /** 48 | * Determines if two values of this type are equal. 49 | * Defaults to using `===`. 50 | * 51 | * @method isEqual 52 | * @param {Any} a Javascript object 53 | * @param {Any} b Javascript object 54 | * @returns {Boolean} Whether or not the objects are equal or not 55 | */ 56 | isEqual: function(a, b) { 57 | return (a === b); 58 | } 59 | }); -------------------------------------------------------------------------------- /src/attribute_type/date.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AttributeType from 'ember-graph/attribute_type/type'; 3 | 4 | /** 5 | * @class DateType 6 | * @extends AttributeType 7 | * @constructor 8 | */ 9 | export default AttributeType.extend({ 10 | 11 | /** 12 | * Converts any Date object, number or string to a timestamp. 13 | * 14 | * @method serialize 15 | * @param {Date|Number|String} date 16 | * @return {Number} 17 | */ 18 | serialize: function(date) { 19 | switch (Ember.typeOf(date)) { 20 | case 'date': 21 | return date.getTime(); 22 | case 'number': 23 | return date; 24 | case 'string': 25 | return new Date(date).getTime(); 26 | default: 27 | return null; 28 | } 29 | }, 30 | 31 | /** 32 | * Converts any numeric or string timestamp to a Date object. 33 | * Everything else gets converted to `null`. 34 | * 35 | * @method deserialize 36 | * @param {Number|String} timestamp 37 | * @return {Date} 38 | */ 39 | deserialize: function(timestamp) { 40 | switch (Ember.typeOf(timestamp)) { 41 | case 'number': 42 | case 'string': 43 | return new Date(timestamp); 44 | default: 45 | return null; 46 | } 47 | }, 48 | 49 | /** 50 | * Converts both arguments to a timestamp, then compares. 51 | * 52 | * @param {Date} a 53 | * @param {Date} b 54 | * @return {Boolean} 55 | */ 56 | isEqual: function(a, b) { 57 | var aNone = (a === null); 58 | var bNone = (b === null); 59 | 60 | if (aNone && bNone) { 61 | return true; 62 | } else if ((aNone && !bNone) || (!aNone && bNone)) { 63 | return false; 64 | } else { 65 | return (new Date(a).getTime() === new Date(b).getTime()); 66 | } 67 | } 68 | }); -------------------------------------------------------------------------------- /test/template.html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ember Graph Tests 6 | 7 | 8 | 9 |
10 |
11 |
12 | 35 | 36 | 37 | 38 | <% if (includeHandlebars) { %> 39 | 40 | <% } %> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | <% _.each(files, function(filepath) { %> 50 | 51 | <% }); %> 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/attribute_type/object.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var type = EG.ObjectType.create(); 5 | 6 | module('Object Attribute Type Test'); 7 | 8 | test('Empty objects are equal', function() { 9 | expect(1); 10 | 11 | ok(type.isEqual({}, {})); 12 | }); 13 | 14 | test('Object constructors work too', function() { 15 | expect(1); 16 | 17 | ok(type.isEqual(Object(), Object())); 18 | }); 19 | 20 | test('Simple objects are equal', function() { 21 | expect(1); 22 | 23 | var a = { foo: 1, bar: 2 }; 24 | var b = { bar: 2, foo: 1 }; 25 | 26 | ok(type.isEqual(a, b)); 27 | }); 28 | 29 | test('Type coercions aren\'t performed', function() { 30 | expect(1); 31 | 32 | var a = { foo: '1' }; 33 | var b = { foo: 1 }; 34 | 35 | ok(!type.isEqual(a, b)); 36 | }); 37 | 38 | test('Arrays compare equally', function() { 39 | expect(1); 40 | 41 | var a = { arr: [1, 2, 3, 4, 5] }; 42 | var b = { arr: [1, 2, 3, 4, 5] }; 43 | 44 | ok(type.isEqual(a, b)); 45 | }); 46 | 47 | test('Nested objects compare equally', function() { 48 | expect(1); 49 | 50 | var a = { 51 | a: null, 52 | o: { 53 | 2: 2, 54 | 3: 4, 55 | 1: 1 56 | } 57 | }; 58 | var b = { 59 | a: null, 60 | o: { 61 | 1: 1, 62 | 2: 2, 63 | 3: 4 64 | } 65 | }; 66 | 67 | ok(type.isEqual(a, b)); 68 | }); 69 | 70 | test('Non-objects always fail', function() { 71 | expect(8); 72 | 73 | ok(!type.isEqual()); 74 | ok(!type.isEqual(null, null)); 75 | ok(!type.isEqual(true, true)); 76 | ok(!type.isEqual(0, 0)); 77 | ok(!type.isEqual('', '')); 78 | ok(!type.isEqual(NaN, NaN)); 79 | ok(!type.isEqual([], [])); 80 | ok(!type.isEqual(new Date(), new Date())); 81 | }); 82 | })(); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | var config = function(moduleName) { 5 | return require('./tasks/options/' + moduleName); 6 | }; 7 | 8 | module.exports = function(grunt) { 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | 12 | build_test_runner: { // eslint-disable-line camelcase 13 | all: ['test/**/*.js'] 14 | }, 15 | 16 | build_release_test_runner: { // eslint-disable-line camelcase 17 | all: ['test/**/*.js'] 18 | }, 19 | 20 | clean: config('clean'), 21 | connect: config('connect'), 22 | eslint: config('eslint'), 23 | groundskeeper: config('groundskeeper'), 24 | qunit: config('qunit'), 25 | sass: config('sass'), 26 | uglify: config('uglify'), 27 | watch: config('watch'), 28 | yuidoc: config('yuidoc') 29 | }); 30 | 31 | grunt.loadNpmTasks('grunt-contrib-clean'); 32 | grunt.loadNpmTasks('grunt-contrib-connect'); 33 | grunt.loadNpmTasks('grunt-contrib-uglify'); 34 | grunt.loadNpmTasks('grunt-contrib-watch'); 35 | grunt.loadNpmTasks('grunt-contrib-yuidoc'); 36 | grunt.loadNpmTasks('grunt-eslint'); 37 | grunt.loadNpmTasks('grunt-groundskeeper'); 38 | grunt.loadNpmTasks('grunt-qunit-istanbul'); 39 | grunt.loadNpmTasks('grunt-sass'); 40 | 41 | grunt.task.loadTasks('./tasks'); 42 | 43 | grunt.registerTask('develop', ['transpile', 'build_test_runner', 'connect:test', 'watch']); 44 | grunt.registerTask('test', ['transpile', 'build_test_runner', 'qunit:all', 'clean:test']); 45 | grunt.registerTask('release', ['transpile', 'groundskeeper:compile', 46 | 'uglify:release', 'build_release_test_runner']); 47 | 48 | grunt.registerTask('build_site', ['yuidoc', 'register_handlebars_helpers', 'convert_documentation_data', 49 | 'setup_site_structure', 'sass', 'build_api_pages']); 50 | }; 51 | -------------------------------------------------------------------------------- /test/model/relationship/creating.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Model Creation Relationship Test', { 7 | setup: function() { 8 | store = setupStore({ 9 | user: EG.Model.extend({ 10 | posts: EG.hasMany({ 11 | relatedType: 'post', 12 | inverse: 'author' 13 | }) 14 | }), 15 | 16 | post: EG.Model.extend({ 17 | author: EG.hasOne({ 18 | relatedType: 'user', 19 | inverse: 'posts' 20 | }) 21 | }) 22 | }, { 23 | adapter: EG.Adapter.extend({ 24 | createRecord: function(record) { 25 | return Em.RSVP.resolve({ 26 | meta: { 27 | createdRecord: { 28 | type: record.get('typeKey'), 29 | id: 'NEW_POST_ID' 30 | } 31 | }, 32 | post: [{ 33 | id: 'NEW_POST_ID', 34 | author: { type: 'user', id: '1' } 35 | }] 36 | }); 37 | } 38 | }) 39 | }); 40 | 41 | store.pushPayload({ 42 | user: [{ 43 | id: '1', 44 | posts: [] 45 | }] 46 | }); 47 | } 48 | }); 49 | 50 | test('Saving a new record updates the relationships with the new ID', function() { 51 | expect(1); 52 | 53 | var post = store.createRecord('post', { author: '1' }); 54 | var tempId = post.get('id'); 55 | 56 | var all = store.get('allRelationships'); 57 | Object.keys(all).forEach(function(id) { 58 | if (all[id].get('id1') === tempId || all[id].get('id2') === tempId) { 59 | ok(true); 60 | } 61 | }); 62 | 63 | post.save().then(function() { 64 | var all = store.get('allRelationships'); 65 | 66 | Object.keys(all).forEach(function(id) { 67 | if (all[id].get('id1') === tempId || all[id].get('id2') === tempId) { 68 | ok(false); 69 | } 70 | }); 71 | }); 72 | }); 73 | })(); -------------------------------------------------------------------------------- /test/model/model.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Model Test', { 7 | setup: function() { 8 | store = setupStore({ 9 | user: EG.Model.extend({ 10 | username: EG.attr({ 11 | type: 'string', 12 | readOnly: true, 13 | defaultValue: function() { 14 | return 'admin'; 15 | } 16 | }), 17 | 18 | posts: EG.hasMany({ 19 | relatedType: 'post', 20 | inverse: 'author', 21 | defaultValue: [] 22 | }) 23 | }), 24 | 25 | post: EG.Model.extend({ 26 | author: EG.hasOne({ 27 | relatedType: 'user', 28 | inverse: 'posts', 29 | readOnly: true, 30 | defaultValue: null 31 | }), 32 | 33 | sharedWith: EG.hasMany({ 34 | relatedType: 'user', 35 | inverse: null, 36 | readOnly: true, 37 | defaultValue: [] 38 | }) 39 | }) 40 | }); 41 | } 42 | }); 43 | 44 | test('Can create a model with read-only attributes', function() { 45 | expect(1); 46 | 47 | var user = store.createRecord('user', { 48 | username: 'gjk' 49 | }); 50 | 51 | strictEqual(user.get('username'), 'gjk'); 52 | }); 53 | 54 | test('Can create a model with read-only hasOne relationship', function() { 55 | expect(2); 56 | 57 | var post1 = store.createRecord('post', { 58 | author: '1' 59 | }); 60 | 61 | deepEqual(post1.get('_author'), { type: 'user', id: '1' }); 62 | 63 | var post2 = store.createRecord('post', { 64 | author: null 65 | }); 66 | 67 | strictEqual(post2.get('_author'), null); 68 | }); 69 | 70 | test('Can create a model with read-only hasMany relationship', function() { 71 | expect(1); 72 | 73 | var post = store.createRecord('post', { 74 | sharedWith: ['1', '2', '3'] 75 | }); 76 | 77 | deepEqual(post.get('_sharedWith').mapBy('id').sort(), ['1', '2', '3'].sort()); 78 | }); 79 | })(); -------------------------------------------------------------------------------- /test/model/relationship/reconnect.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Relationship Reconnection Test', { 7 | setup: function() { 8 | store = setupStore({ 9 | vertex: EG.Model.extend({ 10 | parent: EG.hasOne({ 11 | relatedType: 'vertex', 12 | inverse: 'children' 13 | }), 14 | children: EG.hasMany({ 15 | relatedType: 'vertex', 16 | inverse: 'parent' 17 | }) 18 | }) 19 | }, { 20 | adapter: EG.MemoryAdapter.extend({ 21 | shouldInitializeDatabase: function() { 22 | return true; 23 | }, 24 | getInitialPayload: function() { 25 | return { 26 | vertex: [ 27 | { 28 | id: '1', 29 | parent: null, 30 | children: [ 31 | { type: 'vertex', id: '2' }, 32 | { type: 'vertex', id: '3' } 33 | ] 34 | }, 35 | { 36 | id: '2', 37 | parent: { type: 'vertex', id: '1' }, 38 | children: [] 39 | }, 40 | { 41 | id: '3', 42 | parent: { type: 'vertex', id: '1' }, 43 | children: [] 44 | } 45 | ] 46 | }; 47 | } 48 | }) 49 | }); 50 | } 51 | }); 52 | 53 | asyncTest('Switch parent vertices (reloading the same relationship', 1, function() { 54 | store.find('vertex').then(function() { 55 | var vertex1 = store.getRecord('vertex', '1'); 56 | var vertex2 = store.getRecord('vertex', '2'); 57 | var vertex3 = store.getRecord('vertex', '3'); 58 | 59 | vertex3.setHasOneRelationship('parent', vertex2); 60 | vertex3.save().then(function() { 61 | vertex3.setHasOneRelationship('parent', vertex1); 62 | return vertex3.save(); 63 | }).then(function() { 64 | start(); 65 | ok(true); 66 | }, function(e) { 67 | start(); 68 | throw e; 69 | }); 70 | }); 71 | }); 72 | })(); -------------------------------------------------------------------------------- /tasks/build_test_runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EMBER_VERSIONS = { 4 | '1.8.0': { 5 | development: 'ember-1.8.0/ember.js', 6 | production: 'ember-1.8.0/ember.prod.js' 7 | }, 8 | '1.13.0': { 9 | development: 'ember-1.13.0/ember.debug.js', 10 | production: 'ember-1.13.0/ember.prod.js' 11 | }, 12 | '2.0.0': { 13 | development: 'ember-2.0.0/ember.debug.js', 14 | production: 'ember-2.0.0/ember.prod.js' 15 | } 16 | }; 17 | 18 | module.exports = function(grunt) { 19 | function buildRunners(release) { 20 | var template = grunt.file.read('test/template.html.tmpl'); 21 | 22 | var renderingContext = { 23 | data: { 24 | sourceFile: (release ? 'ember-graph.min.js' : 'ember-graph.js'), 25 | files: this.filesSrc 26 | } 27 | }; 28 | 29 | renderingContext.data.emberFile = 30 | (release ? EMBER_VERSIONS['1.8.0'].production : EMBER_VERSIONS['1.8.0'].development); 31 | renderingContext.data.includeHandlebars = true; 32 | grunt.file.write('test/ember-1.8.0.html', grunt.template.process(template, renderingContext)); 33 | 34 | renderingContext.data.emberFile = 35 | (release ? EMBER_VERSIONS['1.13.0'].production : EMBER_VERSIONS['1.13.0'].development); 36 | renderingContext.data.includeHandlebars = false; 37 | grunt.file.write('test/ember-1.13.0.html', grunt.template.process(template, renderingContext)); 38 | 39 | renderingContext.data.emberFile = 40 | (release ? EMBER_VERSIONS['2.0.0'].production : EMBER_VERSIONS['2.0.0'].development); 41 | renderingContext.data.includeHandlebars = false; 42 | grunt.file.write('test/ember-2.0.0.html', grunt.template.process(template, renderingContext)); 43 | } 44 | 45 | grunt.registerMultiTask('build_test_runner', function() { 46 | buildRunners.call(this, false); 47 | }); 48 | 49 | grunt.registerMultiTask('build_release_test_runner', function() { 50 | buildRunners.call(this, true); 51 | }); 52 | }; -------------------------------------------------------------------------------- /site/templates/api/content_methods.hbs: -------------------------------------------------------------------------------- 1 | {{#each methods}} 2 | {{#if @index}} 3 |
4 | {{/if}} 5 | 6 |
7 |

8 | {{name}} 9 | 10 | 11 | ({{#each parameters}}{{#if @index}}, {{/if}}{{name}}{{/each}}) 12 | 13 | {{#if return.type}} 14 | {{{return.type}}} 15 | {{/if}} 16 | {{#if static}} 17 | static 18 | {{/if}} 19 | {{#if private}} 20 | private 21 | {{/if}} 22 | {{#if protected}} 23 | protected 24 | {{/if}} 25 | {{#if deprecated}} 26 | deprecated 27 | {{/if}} 28 | {{#if abstract}} 29 | abstract 30 | {{/if}} 31 | 32 |

33 | Defined in: 34 | 35 | {{file.path}}:{{file.line}} 36 | 37 | 38 |

{{{description}}}

39 | 40 | {{#if parameters}} 41 |

Parameters:

42 | 55 | {{/if}} 56 | 57 | {{#if return}} 58 |

Returns:

59 | 69 | {{/if}} 70 |
71 | {{/each}} -------------------------------------------------------------------------------- /test/configuration.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | window.setupStore = function(models, options, store) { 5 | options = options || {}; 6 | 7 | var container; 8 | var registry; 9 | 10 | if (Ember.Registry) { 11 | registry = new Ember.Registry(); 12 | container = registry.container(); 13 | } else { 14 | container = new Ember.Container(); 15 | registry = container; 16 | } 17 | 18 | registry.register('adapter:rest', EG.RESTAdapter, { singleton: true }); 19 | registry.register('adapter:memory', EG.MemoryAdapter, { singleton: true }); 20 | registry.register('adapter:local_storage', EG.LocalStorageAdapter, { singleton: true }); 21 | 22 | registry.register('serializer:json', EG.JSONSerializer, { singleton: true }); 23 | registry.register('serializer:ember_graph', EG.EmberGraphSerializer, { singleton: true }); 24 | 25 | registry.register('type:string', EG.StringType, { singleton: true }); 26 | registry.register('type:number', EG.NumberType, { singleton: true }); 27 | registry.register('type:boolean', EG.BooleanType, { singleton: true }); 28 | registry.register('type:date', EG.DateType, { singleton: true }); 29 | registry.register('type:object', EG.ObjectType, { singleton: true }); 30 | registry.register('type:array', EG.ArrayType, { singleton: true }); 31 | 32 | registry.register('store:main', store || EG.Store, { singleton: true }); 33 | store = container.lookup('store:main'); 34 | 35 | registry.injection('adapter', 'store', 'store:main'); 36 | registry.injection('serializer', 'store', 'store:main'); 37 | 38 | if (options.adapter) { 39 | registry.register('adapter:application', options.adapter, { singleton: true }); 40 | } 41 | 42 | Object.keys(models || {}).forEach(function(typeKey) { 43 | registry.register('model:' + typeKey, models[typeKey]); 44 | // Load the model to set the 'typeKey' attributes on it 45 | store.modelFor(typeKey); 46 | }); 47 | 48 | store.__registry__ = registry; 49 | 50 | return store; 51 | }; 52 | })(); -------------------------------------------------------------------------------- /test/relationship/relationship.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | var CLIENT_STATE = EG.Relationship.CLIENT_STATE; 7 | 8 | module('Relationship Object Test', { 9 | setup: function() { 10 | store = setupStore({ 11 | user: EG.Model.extend({ 12 | posts: EG.hasMany({ 13 | relatedType: 'post', 14 | inverse: 'author' 15 | }) 16 | }), 17 | post: EG.Model.extend({ 18 | author: EG.hasOne({ 19 | relatedType: 'user', 20 | inverse: 'posts' 21 | }) 22 | }) 23 | }); 24 | 25 | store.pushPayload({ 26 | user: [{ id: '1', posts: [] }], 27 | post: [{ id: '1', author: null }, { id: '2', author: null }] 28 | }); 29 | } 30 | }); 31 | 32 | test('Relation testing', function() { 33 | expect(15); 34 | 35 | var user = store.getRecord('user', '1'); 36 | var post = store.getRecord('post', '2'); 37 | var other = store.getRecord('post', '1'); 38 | 39 | var relationship = EG.Relationship.create('user', '1', 'posts', 'post', '2', 'author', CLIENT_STATE); 40 | 41 | strictEqual(relationship.otherType(user), 'post'); 42 | strictEqual(relationship.otherType(post), 'user'); 43 | strictEqual(relationship.otherId(user), '2'); 44 | strictEqual(relationship.otherId(post), '1'); 45 | strictEqual(relationship.otherName(user), 'author'); 46 | strictEqual(relationship.otherName(post), 'posts'); 47 | strictEqual(relationship.thisName(user), 'posts'); 48 | strictEqual(relationship.thisName(post), 'author'); 49 | strictEqual(relationship.isConnectedTo(user), true); 50 | strictEqual(relationship.isConnectedTo(post), true); 51 | strictEqual(relationship.isConnectedTo(other), false); 52 | strictEqual(relationship.matchesOneSide('user', '1', 'posts'), true); 53 | strictEqual(relationship.matchesOneSide('post', '2', 'author'), true); 54 | strictEqual(relationship.matchesOneSide('user', '1', 'author'), false); 55 | strictEqual(relationship.matchesOneSide('test', 'foo', 'bar'), false); 56 | }); 57 | })(); 58 | -------------------------------------------------------------------------------- /src/util/string.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | function startsWith(string, prefix) { 4 | return string.indexOf(prefix) === 0; 5 | } 6 | 7 | function endsWith(string, suffix) { 8 | return string.indexOf(suffix, string.length - suffix.length) >= 0; 9 | } 10 | 11 | function capitalize(string) { 12 | return string[0].toLocaleUpperCase() + string.substring(1); 13 | } 14 | 15 | function decapitalize(string) { 16 | return string[0].toLocaleLowerCase() + string.substring(1); 17 | } 18 | 19 | if (Ember.ENV.EXTEND_PROTOTYPES === true || Ember.ENV.EXTEND_PROTOTYPES.String) { 20 | 21 | /** 22 | * Polyfill for String.prototype.startsWith 23 | * 24 | * @method startsWith 25 | * @param {String} prefix 26 | * @return {Boolean} 27 | * @namespace String 28 | */ 29 | if (!String.prototype.startsWith) { 30 | String.prototype.startsWith = function(prefix) { 31 | return startsWith(this, prefix); 32 | }; 33 | } 34 | 35 | /** 36 | *Polyfill for String.prototype.endsWith 37 | * 38 | * @method endsWith 39 | * @param {String} suffix 40 | * @return {Boolean} 41 | * @namespace String 42 | */ 43 | if (!String.prototype.endsWith) { 44 | String.prototype.endsWith = function(suffix) { 45 | return endsWith(this, suffix); 46 | }; 47 | } 48 | 49 | /** 50 | * Capitalizes the first letter of a string. 51 | * 52 | * @method capitalize 53 | * @return {String} 54 | * @namespace String 55 | */ 56 | if (!String.prototype.capitalize) { 57 | String.prototype.capitalize = function() { 58 | return capitalize(this); 59 | }; 60 | } 61 | 62 | /** 63 | * Decapitalizes the first letter of a string. 64 | * 65 | * @method decapitalize 66 | * @return {String} 67 | * @namespace String 68 | */ 69 | if (!String.prototype.decapitalize) { 70 | String.prototype.decapitalize = function() { 71 | return decapitalize(this); 72 | }; 73 | } 74 | } 75 | 76 | export { 77 | startsWith, 78 | endsWith, 79 | capitalize, 80 | decapitalize 81 | }; 82 | -------------------------------------------------------------------------------- /src/before_load.js: -------------------------------------------------------------------------------- 1 | var define = this.define; 2 | var require = this.require; 3 | 4 | var declareModuleLoader = function() { 5 | var DEFINITIONS = {}; 6 | var MODULES = {}; 7 | 8 | var evaluateModule = function(name) { 9 | if (!DEFINITIONS[name]) { 10 | throw new Error('Module not found: ' + name); 11 | } 12 | 13 | var exports = {}; 14 | var dependencies = DEFINITIONS[name].dependencies.map(function(name) { 15 | if (name === 'exports') { 16 | return exports; 17 | } else { 18 | return require(name); 19 | } 20 | }); 21 | 22 | DEFINITIONS[name].definition.apply(null, dependencies); 23 | 24 | MODULES[name] = exports; 25 | DEFINITIONS[name] = null; 26 | 27 | return exports; 28 | }; 29 | 30 | define = function(name, dependencies, definition) { 31 | DEFINITIONS[name] = { 32 | dependencies: dependencies, 33 | definition: definition 34 | }; 35 | }; 36 | 37 | require = function(name) { 38 | if (!MODULES[name]) { 39 | MODULES[name] = evaluateModule(name); 40 | } 41 | 42 | return MODULES[name]; 43 | }; 44 | }; 45 | 46 | var declareGlobalModule = function(global) { 47 | define('ember-graph', ['exports', 'ember'], function(exports, Ember) { 48 | /** 49 | * @module ember-graph 50 | * @main ember-graph 51 | */ 52 | global.EmberGraph = global.EG = Ember['default'].Namespace.create(); 53 | exports['default'] = global.EmberGraph; 54 | }); 55 | }; 56 | 57 | // This is probably a poor way of detecting Ember CLI. Should work for now... 58 | if (!define || !define.petal) { 59 | declareModuleLoader(); 60 | } 61 | 62 | var global = this; 63 | 64 | try { 65 | if (!require('ember')) { 66 | throw null; 67 | } 68 | } catch (e) { 69 | define('ember', ['exports'], function(exports) { 70 | exports['default'] = global.Ember; 71 | }); 72 | } 73 | 74 | try { 75 | if (!require('jquery')) { 76 | throw null; 77 | } 78 | } catch (e) { 79 | define('jquery', ['exports'], function(exports) { 80 | exports['default'] = global.jQuery; 81 | }); 82 | } 83 | 84 | declareGlobalModule(this); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_install: 5 | - npm install -g grunt-cli 6 | - npm install -g bower 7 | install: 8 | - bower install 9 | - npm install 10 | script: 11 | - grunt release 12 | after_success: 13 | - | 14 | cd ${EG_DIR} 15 | grunt upload_builds_to_s3 16 | - | 17 | cd ${EG_DIR}; 18 | grunt build_site; 19 | cd site_build; 20 | git init; 21 | git config user.name "Travis-CI"; 22 | git config user.email "travis@ember-graph.com"; 23 | git add --all .; 24 | git commit -m "Deployed to Github Pages"; 25 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1; 26 | - | 27 | cd ${EG_DIR}; 28 | grunt test; 29 | npm install -g codeclimate-test-reporter; 30 | CODECLIMATE_REPO_TOKEN=${CODECLIMATE_REPO_TOKEN} codeclimate < report/lcov.info; 31 | notifications: 32 | email: false 33 | sudo: false 34 | env: 35 | global: 36 | - GH_REF: github.com/gordonkristan/ember-graph.git 37 | - EG_DIR: /home/travis/build/gordonkristan/ember-graph 38 | # GH_TOKEN 39 | - secure: MgMI2LQwzgjO38L5UKKgkmV0J4d1d+b4DK4kuPYegDxBgADB/o8NocW2LF2IARWGUEhU1BBDmKv45xs1X4TavQJ5/+AM5D/HdyK9VB8cQ+3nHBaqyvO7lURKOi8Pvs+hxiWoTLtXF8rImVtWzoJwcyzNbwY/UceKF3d58PMJ++w= 40 | # CODECLIMATE_REPO_TOKEN 41 | - secure: G0L+6oA48vlg5vCm8qu/PiC2f2Q7aEzXPbD3blLRXYJXJOGz3bsbSl/n4CE8ogU5u1A0jenaSDFggYV0oDoebWxdcBaMaIIsxsQ5GdSb9RWjYDoSemhbUQjmUUN+EP56pNTDxNAfQLJxIbZvCqOUiZ3Q56y948fK4giloWF1844= 42 | # AWS_ACCESS_KEY_ID 43 | - secure: SZjHvDSi1w3p8g+ZR1ls3iSS07uJvXG8y+4r7aSytAtWvtcJibLAHcv9tIgtn901wsCdfuQYmHbjVucI1ASvSdf1TXWGr505UoWY/t3/KEnz2PgFmul87AQUqH0bHkhrRRzh9jlpeRdSwUnLkfvNCbib/LzVdTp5Ft01xseFHwY= 44 | # AWS_SECRET_ACCESS_KEY 45 | - secure: cdTopjWAPZa+ut0tIFKsJQcNSF4ejG7FAhrSKTYh1xZ29lAmLFnOUmBOspgH3bYHpCDS5j1fBRHJTuOJ5xrLSiSPNgEMkPL+833XpyzzBzVrr6gW95r++cA77seD23I4EzTFyCofAdJOpG1wwfx/EOiZrFSaVfiGTGOfehBBzOY= 46 | -------------------------------------------------------------------------------- /site/stylesheets/_api_page.scss: -------------------------------------------------------------------------------- 1 | .side-nav { 2 | ul { 3 | li.active a { 4 | font-weight: bold; 5 | } 6 | 7 | &:first-of-type li:first-of-type h4 { 8 | margin-bottom: 5px; 9 | } 10 | 11 | &:not(:first-of-type) li:first-of-type h4 { 12 | margin-top: 20px; 13 | margin-bottom: 5px; 14 | } 15 | } 16 | } 17 | 18 | .api-contents { 19 | padding: 0 25px; 20 | } 21 | 22 | .api-page-header { 23 | h1 { 24 | i.fa { 25 | margin-right: 15px; 26 | } 27 | 28 | margin-top: 0; 29 | } 30 | 31 | hr { 32 | margin: 0; 33 | } 34 | } 35 | 36 | .api-metadata { 37 | font-size: 1em; 38 | margin-top: 5px; 39 | } 40 | 41 | .api-index { 42 | // ul.nav-tabs { 43 | // border-bottom: 3px solid $theme-color; 44 | // 45 | // li { 46 | // a, a:hover { 47 | // border: none; 48 | // } 49 | // 50 | // a { 51 | // color: $theme-color; 52 | // } 53 | // 54 | // a:hover { 55 | // background-color: #f6f6f6; 56 | // } 57 | // } 58 | // 59 | // li.active { 60 | // a, a:hover { 61 | // background-color: $theme-color; 62 | // color: white; 63 | // border: none; 64 | // border-bottom: 1px solid $theme-color; 65 | // } 66 | // } 67 | // } 68 | 69 | .contents { 70 | h3 { 71 | margin-left: .25em; 72 | margin-right: .25em; 73 | } 74 | 75 | ul { 76 | margin: 1em 1.5em; 77 | } 78 | 79 | border: 1px solid #ddd; 80 | border-top: none; 81 | border-radius: 0 0 4px 4px; 82 | 83 | padding: 1em 1.5em; 84 | } 85 | } 86 | 87 | .api-property, .api-method { 88 | .property-name { 89 | small { 90 | a { 91 | &:hover { 92 | text-decoration: none; 93 | } 94 | } 95 | } 96 | 97 | margin-bottom: 0; 98 | } 99 | 100 | p { 101 | margin: 15px 0; 102 | } 103 | } 104 | 105 | .api-method { 106 | .parameter-list, .return-value { 107 | li { 108 | small { 109 | a, a:hover { 110 | text-decoration: none; 111 | } 112 | 113 | span { 114 | margin-left: 2em; 115 | } 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /src/attribute_type/number.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AttributeType from 'ember-graph/attribute_type/type'; 3 | 4 | /** 5 | * Will coerce any type to a number (0 being the default). `null` is not a valid value. 6 | * 7 | * @class NumberType 8 | * @extends AttributeType 9 | * @constructor 10 | */ 11 | export default AttributeType.extend({ 12 | 13 | /** 14 | * @property defaultValue 15 | * @default 0 16 | * @final 17 | */ 18 | defaultValue: 0, 19 | 20 | /** 21 | * Coerces the given value to a number. 22 | * 23 | * @method serialize 24 | * @param {Number} obj Javascript object 25 | * @return {Number} JSON representation 26 | */ 27 | serialize: function(obj) { 28 | return this.coerceToNumber(obj); 29 | }, 30 | 31 | /** 32 | * Coerces the given value to a number. 33 | * 34 | * @method deserialize 35 | * @param {Number} json JSON representation of object 36 | * @return {Number} Javascript object 37 | */ 38 | deserialize: function(json) { 39 | return this.coerceToNumber(json); 40 | }, 41 | 42 | /** 43 | * If the object passed is a number (and not NaN), it returns 44 | * the object coerced to a number primitive. If the object is 45 | * a string, it attempts to parse it (again, no NaN allowed). 46 | * Otherwise, the default value is returned. 47 | * 48 | * @method coerceToNumber 49 | * @param {Any} obj 50 | * @return {Number} 51 | * @protected 52 | */ 53 | coerceToNumber: function(obj) { 54 | if (this.isValidNumber(obj)) { 55 | return Number(obj).valueOf(); 56 | } 57 | 58 | if (Ember.typeOf(obj) === 'string') { 59 | var parsed = Number(obj).valueOf(); 60 | if (this.isValidNumber(parsed)) { 61 | return parsed; 62 | } 63 | } 64 | 65 | return 0; 66 | }, 67 | 68 | /** 69 | * Determines if the given number is an actual number and finite. 70 | * 71 | * @method isValidNumber 72 | * @param {Number} num 73 | * @return {Boolean} 74 | * @protected 75 | */ 76 | isValidNumber: function(num) { 77 | return (Ember.typeOf(num) === 'number' && !isNaN(num) && isFinite(num)); 78 | } 79 | }); -------------------------------------------------------------------------------- /tasks/register_handlebars_helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Handlebars = require('handlebars'); 4 | 5 | String.prototype.startsWith = String.prototype.startsWith || function(prefix) { 6 | return this.indexOf(prefix) === 0; 7 | }; 8 | 9 | String.prototype.endsWith = String.prototype.endsWith || function(suffix) { 10 | return this.indexOf(suffix, this.length - suffix.length) >= 0; 11 | }; 12 | 13 | module.exports = function(grunt) { 14 | grunt.registerTask('register_handlebars_helpers', function() { 15 | Handlebars.registerHelper('link-to', linkTo); 16 | Handlebars.registerHelper('link-to-class', linkToClass); 17 | Handlebars.registerHelper('link-to-method', linkToMethod); 18 | Handlebars.registerHelper('link-to-property', linkToProperty); 19 | Handlebars.registerHelper('strip-outer-paragraph', stripOuterParagraph); 20 | }); 21 | }; 22 | 23 | function linkTo(content, href) { 24 | return new Handlebars.SafeString('' + content + ''); 25 | } 26 | 27 | function linkToClass(content, name, options) { 28 | if (!options) { 29 | options = name; 30 | name = content; 31 | } 32 | 33 | return new Handlebars.SafeString('' + content + ''); 34 | } 35 | 36 | function linkToMethod(linkName, className, methodName, options) { 37 | return linkToClassItem(linkName, className, methodName, options, 'method'); 38 | } 39 | 40 | function linkToProperty(linkName, className, propertyName, options) { 41 | return linkToClassItem(linkName, className, propertyName, options, 'property'); 42 | } 43 | 44 | function linkToClassItem(linkName, className, itemName, options, type) { 45 | if (!options) { 46 | options = itemName; 47 | itemName = className; 48 | className = linkName; 49 | linkName = itemName; 50 | } 51 | 52 | var html = '' + linkName + ''; 53 | 54 | return new Handlebars.SafeString(html); 55 | } 56 | 57 | function stripOuterParagraph(text) { 58 | text = text.trim(); 59 | 60 | if (text.startsWith('

') && text.endsWith('

')) { 61 | text = text.substring(3, text.length - 4); 62 | } 63 | 64 | return new Handlebars.SafeString(text); 65 | } -------------------------------------------------------------------------------- /src/adapter/memory.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter'; 3 | 4 | const Promise = Ember.RSVP.Promise; 5 | 6 | /** 7 | * This adapter stores all of your changes in memory, mainly for testing 8 | * purposes. To initialize the memory with an initial data set, override 9 | * the {{link-to-method 'MemoryAdapter' 'getInitialPayload'}} hook to 10 | * return the data that you want to load into memory. 11 | * 12 | * To customize the the behavior for getting or saving records, you can 13 | * override any of the following methods: 14 | * {{link-to-method 'MemoryAdapter' 'serverFindRecord'}}, 15 | * {{link-to-method 'MemoryAdapter' 'serverFindMany'}}, 16 | * {{link-to-method 'MemoryAdapter' 'serverFindAll'}}, 17 | * {{link-to-method 'MemoryAdapter' 'serverCreateRecord'}}, 18 | * {{link-to-method 'MemoryAdapter' 'serverDeleteRecord'}}, 19 | * {{link-to-method 'MemoryAdapter' 'serverUpdateRecord'}}. 20 | * 21 | * @class MemoryAdapter 22 | * @extends EmberGraphAdapter 23 | */ 24 | export default EmberGraphAdapter.extend({ 25 | 26 | database: null, 27 | 28 | getDatabase: function() { 29 | try { 30 | var database = this.get('database'); 31 | 32 | if (database) { 33 | return Promise.resolve(database); 34 | } else { 35 | return Promise.resolve({ records: {}, relationships: [] }); 36 | } 37 | } catch (error) { 38 | return Promise.reject(error); 39 | } 40 | }, 41 | 42 | setDatabase: function(database) { 43 | try { 44 | this.set('database', database); 45 | return Promise.resolve(database); 46 | } catch (error) { 47 | return Promise.reject(error); 48 | } 49 | }, 50 | 51 | shouldInitializeDatabase: function() { 52 | return true; 53 | }, 54 | 55 | /** 56 | * Initializes the database (if configured to do so). 57 | * This function is called at adapter initialization 58 | * (which is probably when it's looked up by the container). 59 | * 60 | * @method initializeDatabase 61 | * @private 62 | */ 63 | initializeDatabase: function() { 64 | this._super(); 65 | }, 66 | 67 | initializeDatabaseOnInit: Ember.on('init', function() { 68 | this.initializeDatabase(); 69 | }) 70 | 71 | }); -------------------------------------------------------------------------------- /site/templates/api/base.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ember-Graph 9 | 10 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 49 | 50 | {{{body}}} 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /test/model/relationship/reloading.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var store; 4 | 5 | module('Relationship Reload Test', { 6 | setup: function() { 7 | store = setupStore({ 8 | user: EG.Model.extend({ 9 | posts: EG.hasMany({ 10 | relatedType: 'post', 11 | inverse: 'author' 12 | }) 13 | }), 14 | post: EG.Model.extend({ 15 | author: EG.hasOne({ 16 | relatedType: 'user', 17 | inverse: 'posts' 18 | }) 19 | }) 20 | }); 21 | 22 | store.pushPayload({ 23 | user: [ 24 | { 25 | id: '1', 26 | posts: [ 27 | { type: 'post', id: '1' }, 28 | { type: 'post', id: '2' }, 29 | { type: 'post', id: '3' } 30 | ] 31 | } 32 | ], 33 | post: [ 34 | { id: '1', author: { type: 'user', id: '1' } }, 35 | { id: '2', author: { type: 'user', id: '1' } }, 36 | { id: '3', author: { type: 'user', id: '1' } } 37 | ] 38 | }); 39 | } 40 | }); 41 | 42 | asyncTest('Underscore observer fires when new relationships are added', function() { 43 | expect(1); 44 | 45 | var model = store.getRecord('user', '1'); 46 | model.get('_posts'); 47 | 48 | model.addObserver('_posts', model, function() { 49 | start(); 50 | ok(true); 51 | }); 52 | 53 | store.pushPayload({ 54 | user: [{ 55 | id: '1', 56 | posts: [ 57 | { type: 'post', id: '1' }, 58 | { type: 'post', id: '2' }, 59 | { type: 'post', id: '3' }, 60 | { type: 'post', id: '4' } 61 | ] 62 | }], 63 | post: [ 64 | { id: '4', author: { type: 'user', id: '1' } } 65 | ] 66 | }); 67 | }); 68 | 69 | asyncTest('Relationship observer fires when new relationships are added', function() { 70 | expect(1); 71 | 72 | var model = store.getRecord('user', '1'); 73 | model.get('posts'); 74 | 75 | model.addObserver('posts', model, function() { 76 | start(); 77 | ok(true); 78 | }); 79 | 80 | store.pushPayload({ 81 | user: [{ 82 | id: '1', 83 | posts: [ 84 | { type: 'post', id: '1' }, 85 | { type: 'post', id: '2' }, 86 | { type: 'post', id: '3' }, 87 | { type: 'post', id: '4' } 88 | ] 89 | }], 90 | post: [ 91 | { id: '4', author: { type: 'user', id: '1' } } 92 | ] 93 | }); 94 | }); 95 | })(); -------------------------------------------------------------------------------- /tasks/upload_builds_to_s3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var AWS = require('aws-sdk'); 5 | var AdmZip = require('adm-zip'); 6 | var execSync = require('child_process').execSync; 7 | 8 | module.exports = function(grunt) { 9 | grunt.registerTask('upload_builds_to_s3', function() { 10 | var done = this.async(); 11 | var hash = execSync('git rev-parse HEAD').toString().trim(); 12 | var bowerConfig = fs.readFileSync('./bower.json', { encoding: 'utf8' }); 13 | var debugBuild = fs.readFileSync('./dist/ember-graph.js', { encoding: 'utf8' }); 14 | var productionBuild = fs.readFileSync('./dist/ember-graph.prod.js', { encoding: 'utf8' }); 15 | var minifiedBuild = fs.readFileSync('./dist/ember-graph.min.js', { encoding: 'utf8' }); 16 | 17 | var zip = new AdmZip(); 18 | zip.addFile('bower.json', new Buffer(bowerConfig)); 19 | zip.addFile('ember-graph.js', new Buffer(debugBuild)); 20 | zip.addFile('ember-graph.prod.js', new Buffer(productionBuild)); 21 | zip.addFile('ember-graph.min.js', new Buffer(minifiedBuild)); 22 | var archive = zip.toBuffer(); 23 | 24 | var count = 0; 25 | var counter = function(success, fileName) { 26 | count = count + 1; 27 | 28 | if (!success) { 29 | console.log('Error uploading file to S3: ' + fileName); 30 | } 31 | 32 | if (count >= 8) { 33 | done(); 34 | } 35 | }; 36 | 37 | var s3 = new AWS.S3(); 38 | uploadFile(s3, 'latest/ember-graph.js', debugBuild, counter); 39 | uploadFile(s3, 'latest/ember-graph.prod.js', productionBuild, counter); 40 | uploadFile(s3, 'latest/ember-graph.min.js', minifiedBuild, counter); 41 | uploadFile(s3, 'latest/ember-graph.zip', archive, counter); 42 | 43 | uploadFile(s3, hash + '/ember-graph.min.js', minifiedBuild, counter); 44 | uploadFile(s3, hash + '/ember-graph.js', debugBuild, counter); 45 | uploadFile(s3, hash + '/ember-graph.prod.js', productionBuild, counter); 46 | uploadFile(s3, hash + '/ember-graph.zip', archive, counter); 47 | }); 48 | }; 49 | 50 | function uploadFile(s3, fileName, contents, callback) { 51 | s3.putObject({ 52 | ACL: 'public-read', 53 | Body: contents, 54 | Bucket: 'ember-graph-builds', 55 | ContentType: (fileName.endsWith('.zip') ? 'application/zip' : 'application/javascript'), 56 | Key: fileName 57 | }, function(err, data) { 58 | callback(!err, fileName); 59 | }); 60 | } -------------------------------------------------------------------------------- /src/store/record_cache.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { PromiseObject } from 'ember-graph/data/promise_object'; 4 | import { computed } from 'ember-graph/util/computed'; 5 | 6 | 7 | export default Ember.Object.extend({ 8 | 9 | cacheTimeout: computed('_cacheTimeout', { 10 | get() { 11 | return this.get('_cacheTimeout'); 12 | }, 13 | set(key, value) { 14 | this.set('_cacheTimeout', typeof value === 'number' ? value : Infinity); 15 | } 16 | }), 17 | 18 | records: {}, 19 | 20 | liveRecordArrays: {}, 21 | 22 | init() { 23 | this.setProperties({ 24 | _cacheTimeout: Infinity, 25 | records: {}, 26 | liveRecordArrays: {} 27 | }); 28 | }, 29 | 30 | getRecord(typeKey, id) { 31 | const key = `${typeKey}:${id}`; 32 | const records = this.get('records'); 33 | 34 | if (records[key] && records[key].timestamp >= (new Date()).getTime() - this.get('cacheTimeout')) { 35 | return records[key].record; 36 | } 37 | 38 | return null; 39 | }, 40 | 41 | getRecords(typeKey) { 42 | const records = this.get('records'); 43 | const found = []; 44 | const cutoff = (new Date()).getTime() - this.get('cacheTimeout'); 45 | 46 | Object.keys(records).forEach((key) => { 47 | if (key.indexOf(typeKey) === 0 && records[key].timestamp >= cutoff) { 48 | found.push(records[key].record); 49 | } 50 | }); 51 | 52 | return found; 53 | }, 54 | 55 | storeRecord(record) { 56 | if (PromiseObject.detectInstance(record)) { 57 | record = record.getModel(); 58 | } 59 | 60 | const typeKey = record.get('typeKey'); 61 | 62 | const records = this.get('records'); 63 | records[`${typeKey}:${record.get('id')}`] = { 64 | record, 65 | timestamp: (new Date()).getTime() 66 | }; 67 | 68 | const liveRecordArrays = this.get('liveRecordArrays'); 69 | liveRecordArrays[typeKey] = liveRecordArrays[typeKey] || Ember.A(); 70 | if (!liveRecordArrays[typeKey].includes(record)) { 71 | liveRecordArrays[typeKey].addObject(record); 72 | } 73 | }, 74 | 75 | deleteRecord(typeKey, id) { 76 | const records = this.get('records'); 77 | delete records[`${typeKey}:${id}`]; 78 | }, 79 | 80 | getLiveRecordArray(typeKey) { 81 | const liveRecordArrays = this.get('liveRecordArrays'); 82 | liveRecordArrays[typeKey] = liveRecordArrays[typeKey] || Ember.A(); 83 | return liveRecordArrays[typeKey]; 84 | } 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /site/stylesheets/_highlight.scss: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-template_comment, 15 | .diff .hljs-header, 16 | .hljs-javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | .hljs-keyword, 22 | .css .rule .hljs-keyword, 23 | .hljs-winutils, 24 | .javascript .hljs-title, 25 | .nginx .hljs-title, 26 | .hljs-subst, 27 | .hljs-request, 28 | .hljs-status { 29 | color: #333; 30 | font-weight: bold 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #099; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-phpdoc, 42 | .tex .hljs-formula { 43 | color: #d14 44 | } 45 | 46 | .hljs-title, 47 | .hljs-id, 48 | .coffeescript .hljs-params, 49 | .scss .hljs-preprocessor { 50 | color: #900; 51 | font-weight: bold 52 | } 53 | 54 | .javascript .hljs-title, 55 | .lisp .hljs-title, 56 | .clojure .hljs-title, 57 | .hljs-subst { 58 | font-weight: normal 59 | } 60 | 61 | .hljs-class .hljs-title, 62 | .haskell .hljs-type, 63 | .vhdl .hljs-literal, 64 | .tex .hljs-command { 65 | color: #458; 66 | font-weight: bold 67 | } 68 | 69 | .hljs-tag, 70 | .hljs-tag .hljs-title, 71 | .hljs-rules .hljs-property, 72 | .django .hljs-tag .hljs-keyword { 73 | color: #000080; 74 | font-weight: normal 75 | } 76 | 77 | .hljs-attribute, 78 | .hljs-variable, 79 | .lisp .hljs-body { 80 | color: #008080 81 | } 82 | 83 | .hljs-regexp { 84 | color: #009926 85 | } 86 | 87 | .hljs-symbol, 88 | .ruby .hljs-symbol .hljs-string, 89 | .lisp .hljs-keyword, 90 | .tex .hljs-special, 91 | .hljs-prompt { 92 | color: #990073 93 | } 94 | 95 | .hljs-built_in, 96 | .lisp .hljs-title, 97 | .clojure .hljs-built_in { 98 | color: #0086b3 99 | } 100 | 101 | .hljs-preprocessor, 102 | .hljs-pragma, 103 | .hljs-pi, 104 | .hljs-doctype, 105 | .hljs-shebang, 106 | .hljs-cdata { 107 | color: #999; 108 | font-weight: bold 109 | } 110 | 111 | .hljs-deletion { 112 | background: #fdd 113 | } 114 | 115 | .hljs-addition { 116 | background: #dfd 117 | } 118 | 119 | .diff .hljs-change { 120 | background: #0086b3 121 | } 122 | 123 | .hljs-chunk { 124 | color: #aaa 125 | } 126 | -------------------------------------------------------------------------------- /src/relationship/relationship_hash.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /** 4 | * Define HashMap class 5 | * 6 | * Simple use of associative array as hash table allowing buckets w/ 7 | * chaining for duplicates. 8 | */ 9 | 10 | var RelationshipNode = Ember.Object.extend({ 11 | 12 | next: undefined, 13 | prev: undefined, 14 | item: undefined, 15 | 16 | initialize: Ember.on('init', function() { 17 | this.setProperties({ 18 | next: undefined, 19 | prev: undefined, 20 | item: undefined 21 | }); 22 | }) 23 | 24 | }); 25 | 26 | 27 | var RelationshipHash = Ember.Object.extend({ 28 | 29 | buckets: {}, 30 | 31 | initialize: Ember.on('init', function() { 32 | this.setProperties({ 33 | buckets: [] 34 | }); 35 | }), 36 | 37 | add(item, ids) { 38 | for (var i = 0; i < ids.length; ++i) { 39 | var newNode = RelationshipNode.create(); 40 | newNode.item = item; 41 | if (this.buckets[ids[i]]) { 42 | // Push onto head of chain 43 | newNode.next = this.buckets[ids[i]]; 44 | this.buckets[ids[i]].prev = newNode; 45 | } 46 | this.buckets[ids[i]] = newNode; 47 | } 48 | }, 49 | 50 | findAllByKeys(ids) { 51 | var retVal = []; 52 | for (var i = 0; i < ids.length; ++i) { 53 | var current = this.buckets[ids[i]]; 54 | while (current) { 55 | retVal[current.item.id] = current.item; 56 | current = current.next; 57 | } 58 | } 59 | return retVal; 60 | }, 61 | 62 | remove(item, ids) { 63 | let removed; 64 | for (var i = 0; i < ids.length; ++i) { 65 | var current = this.buckets[ids[i]]; 66 | while (current) { 67 | var next = current.next; 68 | if (current.item === item) { 69 | if (!current.next && !current.prev) { 70 | var deleteme = this.buckets[ids[i]]; 71 | this.buckets[ids[i]] = undefined; 72 | removed = 1; 73 | // Don't forget to delete last node removed 74 | deleteme.destroy(); 75 | } else { 76 | if (current.next) { 77 | current.next.prev = current.prev; 78 | } 79 | if (current.prev) { 80 | current.prev.next = current.next; 81 | } else { 82 | // Removing head 83 | this.buckets[ids[i]] = current.next; 84 | } 85 | current.prev = undefined; 86 | current.next = undefined; 87 | removed = 1; 88 | // Delete node when removed 89 | current.destroy(); 90 | } 91 | } 92 | current = next; 93 | } 94 | } 95 | return removed; 96 | } 97 | }); 98 | 99 | export default RelationshipHash; -------------------------------------------------------------------------------- /src/adapter/local_storage.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter'; 3 | 4 | const Promise = Ember.RSVP.Promise; 5 | 6 | /** 7 | * This adapter will store all of your application data in the browser's 8 | * localStorage. This adapter can be useful for caching data on the client, 9 | * or for testing purposes. If you want to initialize the localStorage 10 | * with an initial data set, override the 11 | * {{link-to-method 'LocalStorageAdapter' 'shouldInitializeDatabase'}} and 12 | * {{link-to-method 'LocalStorageAdapter' 'getInitialPayload'}} hooks. 13 | * 14 | * To customize the the behavior for getting or saving records, you can 15 | * override any of the following methods: 16 | * {{link-to-method 'LocalStorageAdapter' 'serverFindRecord'}}, 17 | * {{link-to-method 'LocalStorageAdapter' 'serverFindMany'}}, 18 | * {{link-to-method 'LocalStorageAdapter' 'serverFindAll'}}, 19 | * {{link-to-method 'LocalStorageAdapter' 'serverCreateRecord'}}, 20 | * {{link-to-method 'LocalStorageAdapter' 'serverDeleteRecord'}}, 21 | * {{link-to-method 'LocalStorageAdapter' 'serverUpdateRecord'}}. 22 | * 23 | * @class LocalStorageAdapter 24 | * @extends EmberGraphAdapter 25 | */ 26 | export default EmberGraphAdapter.extend({ 27 | 28 | /** 29 | * @property localStorageKey 30 | * @default 'ember-graph.db' 31 | * @final 32 | * @protected 33 | */ 34 | localStorageKey: 'ember-graph.db', 35 | 36 | getDatabase: function() { 37 | try { 38 | var key = this.get('localStorageKey'); 39 | var value = localStorage[key]; 40 | 41 | if (value) { 42 | return Promise.resolve(JSON.parse(value)); 43 | } else { 44 | return Promise.resolve({ records: {}, relationships: [] }); 45 | } 46 | } catch (error) { 47 | return Promise.reject(error); 48 | } 49 | }, 50 | 51 | setDatabase: function(db) { 52 | try { 53 | var key = this.get('localStorageKey'); 54 | localStorage[key] = JSON.stringify(db); 55 | return Promise.resolve(db); 56 | } catch (error) { 57 | return Promise.reject(error); 58 | } 59 | }, 60 | 61 | /** 62 | * Initializes the database (if configured to do so). 63 | * This function is called at adapter initialization 64 | * (which is probably when it's looked up by the container). 65 | * 66 | * @method initializeDatabase 67 | * @private 68 | */ 69 | initializeDatabase: function() { 70 | this._super(); 71 | }, 72 | 73 | initializeDatabaseOnInit: Ember.on('init', function() { 74 | this.initializeDatabase(); 75 | }) 76 | 77 | }); -------------------------------------------------------------------------------- /test/store/record_cache.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | var cache; 6 | 7 | module('Record Cache Test', { 8 | setup: function() { 9 | cache = EG.RecordCache.create(); 10 | 11 | store = setupStore({ 12 | item: EG.Model.extend() 13 | }); 14 | 15 | store.pushPayload({ 16 | item: [ 17 | { id: '1' }, 18 | { id: '2' }, 19 | { id: '3' }, 20 | { id: '4' }, 21 | { id: '5' } 22 | ] 23 | }); 24 | } 25 | }); 26 | 27 | test('Store and fetch a record', function() { 28 | expect(3); 29 | 30 | var item1 = store.getRecord('item', '1'); 31 | var item2 = store.getRecord('item', '2'); 32 | var item3 = store.getRecord('item', '3'); 33 | 34 | cache.storeRecord(item1); 35 | cache.storeRecord(item2); 36 | cache.storeRecord(item3); 37 | 38 | strictEqual(cache.getRecord('item', '1'), item1); 39 | strictEqual(cache.getRecord('item', '2'), item2); 40 | strictEqual(cache.getRecord('item', '3'), item3); 41 | }); 42 | 43 | test('Fetch all records of a type', function() { 44 | expect(3); 45 | 46 | var item1 = store.getRecord('item', '1'); 47 | var item2 = store.getRecord('item', '2'); 48 | var item3 = store.getRecord('item', '3'); 49 | 50 | cache.storeRecord(item1); 51 | cache.storeRecord(item2); 52 | cache.storeRecord(item3); 53 | 54 | cache.getRecords('item').forEach(function(record) { 55 | ok(record === item1 || record === item2 || record === item3); 56 | }); 57 | }); 58 | 59 | test('Records expire after timeout', function() { 60 | expect(2); 61 | 62 | cache = EG.RecordCache.create({ cacheTimeout: 10 }); 63 | var item1 = store.getRecord('item', '1'); 64 | cache.storeRecord(item1); 65 | strictEqual(cache.getRecord('item', '1'), item1); 66 | 67 | var then = Date.now(); 68 | while (Date.now() - then <= 10) {} // eslint-disable-line no-empty 69 | 70 | strictEqual(cache.getRecord('item', '1'), null); 71 | }); 72 | 73 | test('Live record arrays are kept up to date', function() { 74 | expect(7); 75 | 76 | var item1 = store.getRecord('item', '1'); 77 | var item2 = store.getRecord('item', '2'); 78 | var item3 = store.getRecord('item', '3'); 79 | 80 | var items = cache.getLiveRecordArray('item'); 81 | strictEqual(items.length, 0); 82 | 83 | cache.storeRecord(item1); 84 | strictEqual(items.length, 1); 85 | ok(items.contains(item1)); 86 | 87 | cache.storeRecord(item2); 88 | strictEqual(items.length, 2); 89 | ok(items.contains(item2)); 90 | 91 | cache.storeRecord(item3); 92 | strictEqual(items.length, 3); 93 | ok(items.contains(item3)); 94 | }); 95 | })(); -------------------------------------------------------------------------------- /src/initializer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraph from 'ember-graph'; 3 | 4 | Ember.onLoad('Ember.Application', function(Application) { 5 | Application.initializer({ 6 | name: 'ember-graph', 7 | initialize() { 8 | let application = arguments[1] || arguments[0]; 9 | Ember.libraries.register('Ember Graph'); 10 | 11 | const useService = !!Ember.Service; 12 | 13 | application.register('store:main', application.Store || EmberGraph.Store); 14 | 15 | if (useService) { 16 | application.register('service:store', application.Store || EmberGraph.Store); 17 | } 18 | 19 | application.register('adapter:rest', EmberGraph.RESTAdapter); 20 | application.register('adapter:memory', EmberGraph.MemoryAdapter); 21 | application.register('adapter:local_storage', EmberGraph.LocalStorageAdapter); 22 | 23 | application.register('serializer:json', EmberGraph.JSONSerializer); 24 | application.register('serializer:ember_graph', EmberGraph.EmberGraphSerializer); 25 | 26 | application.register('type:string', EmberGraph.StringType); 27 | application.register('type:number', EmberGraph.NumberType); 28 | application.register('type:boolean', EmberGraph.BooleanType); 29 | application.register('type:date', EmberGraph.DateType); 30 | application.register('type:object', EmberGraph.ObjectType); 31 | application.register('type:array', EmberGraph.ArrayType); 32 | 33 | application.inject('controller', 'store', 'store:main'); 34 | application.inject('route', 'store', 'store:main'); 35 | application.inject('adapter', 'store', 'store:main'); 36 | application.inject('serializer', 'store', 'store:main'); 37 | 38 | if (useService) { 39 | application.inject('controller', 'store', 'service:store'); 40 | application.inject('route', 'store', 'service:store'); 41 | application.inject('adapter', 'store', 'service:store'); 42 | application.inject('serializer', 'store', 'service:store'); 43 | } 44 | 45 | if (EmberGraph.DataAdapter) { 46 | application.register('data-adapter:main', EmberGraph.DataAdapter); 47 | application.inject('data-adapter', 'store', 'store:main'); 48 | } 49 | } 50 | }); 51 | 52 | if (Application.instanceInitializer) { 53 | Application.instanceInitializer({ 54 | name: 'ember-graph', 55 | initialize(instance) { 56 | const application = instance.lookup('application:main'); 57 | const store = instance.lookup('store:main'); 58 | application.set('store', store); 59 | } 60 | }); 61 | } else { 62 | Application.initializer({ 63 | name: 'ember-graph-store', 64 | initialize(container, application) { 65 | const store = container.lookup('store:main'); 66 | application.set('store', store); 67 | } 68 | }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /test/model/equality.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Model Equality Test', { 7 | setup: function() { 8 | store = setupStore({ 9 | user: EG.Model.extend({ 10 | posts: EG.hasMany({ 11 | relatedType: 'post', 12 | inverse: 'author', 13 | isRequired: false 14 | }) 15 | }), 16 | 17 | post: EG.Model.extend({ 18 | author: EG.hasOne({ 19 | relatedType: 'user', 20 | inverse: 'posts', 21 | isRequired: false 22 | }), 23 | 24 | tags: EG.hasMany({ 25 | relatedType: 'tag', 26 | inverse: null, 27 | isRequired: false, 28 | defaultValue: ['0'] 29 | }) 30 | }), 31 | 32 | tag: EG.Model.extend() 33 | }, { 34 | adapter: EG.Adapter.extend({ 35 | deleteRecord: function(record) { 36 | return Em.RSVP.Promise.resolve({}); 37 | } 38 | }) 39 | }); 40 | 41 | store.pushPayload({ 42 | user: [ 43 | { 44 | id: '1', 45 | posts: [ 46 | { type: 'post', id: '1' }, 47 | { type: 'post', id: '2' }, 48 | { type: 'post', id: '3' } 49 | ] 50 | } 51 | ], 52 | post: [ 53 | { id: '1', author: { type: 'use', id: '1' }, tags: [] }, 54 | { id: '1', author: { type: 'use', id: '1' }, tags: [] }, 55 | { id: '1', author: { type: 'use', id: '1' }, tags: [] } 56 | ] 57 | }); 58 | } 59 | }); 60 | 61 | test('The same model compares correctly', function() { 62 | expect(3); 63 | 64 | var user = store.getRecord('user', '1'); 65 | 66 | ok(user.isEqual(user)); 67 | ok(EG.Model.isEqual(user, user)); 68 | ok(Em.isEqual(user, user)); 69 | }); 70 | 71 | test('A model compares to non-model object correctly', function() { 72 | expect(3); 73 | 74 | var user = store.getRecord('user', '1'); 75 | 76 | ok(!user.isEqual()); 77 | ok(!user.isEqual(null)); 78 | ok(!user.isEqual('')); 79 | }); 80 | 81 | asyncTest('A proxy is equal to the real record', function() { 82 | expect(3); 83 | 84 | var realUser = store.getRecord('user', '1'); 85 | var proxyUser = EG.PromiseObject.create({ promise: Em.RSVP.resolve(realUser) }); 86 | 87 | proxyUser.then(function() { 88 | start(); 89 | 90 | ok(realUser.isEqual(proxyUser)); 91 | ok(EG.Model.isEqual(realUser, proxyUser)); 92 | ok(EG.Model.isEqual(proxyUser, realUser)); 93 | }); 94 | }); 95 | 96 | test('Objects of different types with the same ID aren\'t equal', function() { 97 | expect(2); 98 | 99 | var user = store.getRecord('user', '1'); 100 | var post = store.getRecord('post', '1'); 101 | 102 | ok(!user.isEqual(post)); 103 | ok(!post.isEqual(user)); 104 | }); 105 | })(); -------------------------------------------------------------------------------- /src/util/copy.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Copyable from 'ember-graph/util/copyable'; 3 | 4 | const EmberObject = Ember.Object; 5 | 6 | function _copy(obj, deep, seen, copies) { 7 | // primitive data types are immutable, just return them. 8 | if (typeof obj !== 'object' || obj === null) { 9 | return obj; 10 | } 11 | 12 | let ret, loc; 13 | 14 | // avoid cyclical loops 15 | if (deep && (loc = seen.indexOf(obj)) >= 0) { 16 | return copies[loc]; 17 | } 18 | 19 | // IMPORTANT: this specific test will detect a native array only. Any other 20 | // object will need to implement Copyable. 21 | if (Array.isArray(obj)) { 22 | ret = obj.slice(); 23 | 24 | if (deep) { 25 | loc = ret.length; 26 | 27 | while (--loc >= 0) { 28 | ret[loc] = _copy(ret[loc], deep, seen, copies); 29 | } 30 | } 31 | } else if (Copyable.detect(obj)) { 32 | ret = obj.copy(deep, seen, copies); 33 | } else if (obj instanceof Date) { 34 | ret = new Date(obj.getTime()); 35 | } else { 36 | Ember.assert( 37 | 'Cannot clone an EmberObject that does not implement Copyable', 38 | !(obj instanceof EmberObject) 39 | ); 40 | 41 | ret = {}; 42 | let key; 43 | for (key in obj) { 44 | // support Null prototype 45 | if (!Object.prototype.hasOwnProperty.call(obj, key)) { 46 | continue; 47 | } 48 | 49 | // Prevents browsers that don't respect non-enumerability from 50 | // copying internal Ember properties 51 | if (key.substring(0, 2) === '__') { 52 | continue; 53 | } 54 | 55 | ret[key] = deep ? _copy(obj[key], deep, seen, copies) : obj[key]; 56 | } 57 | } 58 | if (deep) { 59 | seen.push(obj); 60 | copies.push(ret); 61 | } 62 | 63 | return ret; 64 | } 65 | 66 | /** 67 | Creates a shallow copy of the passed object. A deep copy of the object is 68 | returned if the optional `deep` argument is `true`. 69 | 70 | If the passed object implements the `Copyable` interface, then this 71 | function will delegate to the object's `copy()` method and return the 72 | result. See `Copyable` for further details. 73 | 74 | For primitive values (which are immutable in JavaScript), the passed object 75 | is simply returned. 76 | 77 | @function copy 78 | @param {Object} obj The object to clone 79 | @param {Boolean} [deep=false] If true, a deep copy of the object is made. 80 | @return {Object} The copied object 81 | */ 82 | export default function copy(obj, deep) { 83 | // fast paths 84 | if ('object' !== typeof obj || obj === null) { 85 | return obj; // can't copy primitives 86 | } 87 | 88 | if (!Array.isArray(obj) && Copyable.detect(obj)) { 89 | return obj.copy(deep); 90 | } 91 | 92 | return _copy(obj, deep, deep ? [] : null, deep ? [] : null); 93 | } 94 | -------------------------------------------------------------------------------- /src/attribute_type/object.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraphSet from 'ember-graph/util/set'; 3 | import AttributeType from 'ember-graph/attribute_type/type'; 4 | 5 | /** 6 | * @class ObjectType 7 | * @extends AttributeType 8 | * @constructor 9 | */ 10 | export default AttributeType.extend({ 11 | 12 | /** 13 | * If the value is a JSON object, it's returned. 14 | * Otherwise, it serializes to `null`. 15 | * 16 | * @method serialize 17 | * @param {Object} obj 18 | * @return {Object} 19 | */ 20 | serialize: function(obj) { 21 | if (this.isObject(obj)) { 22 | try { 23 | JSON.stringify(obj); 24 | return obj; 25 | } catch (e) { 26 | return null; 27 | } 28 | } else { 29 | return null; 30 | } 31 | }, 32 | 33 | /** 34 | * Returns the value if it's an object, `null` otherwise. 35 | * 36 | * @method deserialize 37 | * @param {Object} json 38 | * @return {Object} 39 | */ 40 | deserialize: function(json) { 41 | if (this.isObject(json)) { 42 | return json; 43 | } else { 44 | return null; 45 | } 46 | }, 47 | 48 | /** 49 | * Checks for equality using 50 | * {{link-to-method 'ObjectType' 'deepCompare'}}. 51 | * 52 | * @method isEqual 53 | * @param {Object} a 54 | * @param {Object} b 55 | * @return {Boolean} 56 | */ 57 | isEqual: function(a, b) { 58 | if (!this.isObject(a) || !this.isObject(b)) { 59 | return false; 60 | } 61 | 62 | return this.deepCompare(a, b); 63 | }, 64 | 65 | /** 66 | * Determines if the value is a plain Javascript object. 67 | * 68 | * @method isObject 69 | * @param {Object} obj 70 | * @return {Boolean} 71 | */ 72 | isObject: function(obj) { 73 | return !Ember.isNone(obj) && Ember.typeOf(obj) === 'object' && obj.constructor === Object; 74 | }, 75 | 76 | /** 77 | * Performs a deep comparison on the objects, iterating 78 | * objects and arrays, and using `===` on primitives. 79 | * 80 | * @method deepCompare 81 | * @param {Object} a 82 | * @param {Object} b 83 | * @return {Boolean} 84 | */ 85 | deepCompare: function(a, b) { 86 | if (this.isObject(a) && this.isObject(b)) { 87 | var aKeys = EmberGraphSet.create(); 88 | var bKeys = EmberGraphSet.create(); 89 | 90 | aKeys.addObjects(Object.keys(a)); 91 | bKeys.addObjects(Object.keys(b)); 92 | 93 | if (!aKeys.isEqual(bKeys)) { 94 | return false; 95 | } 96 | 97 | var keys = Object.keys(a); 98 | 99 | for (var i = 0; i < keys.length; i = i + 1) { 100 | if (!this.deepCompare(a[keys[i]], b[keys[i]])) { 101 | return false; 102 | } 103 | } 104 | 105 | return true; 106 | } else if (Ember.isArray(a) && Ember.isArray(b)) { 107 | return Ember.compare(a, b) === 0; 108 | } else { 109 | return (a === b); 110 | } 111 | } 112 | }); -------------------------------------------------------------------------------- /src/model/states.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { computed } from 'ember-graph/util/computed'; 4 | import { startsWith } from 'ember-graph/util/string'; 5 | 6 | export default { 7 | 8 | /** 9 | * Denotes that the record is currently being deleted, but the server hasn't responded yet. 10 | * 11 | * @property isDeleting 12 | * @type Boolean 13 | * @final 14 | * @for Model 15 | */ 16 | isDeleting: false, 17 | 18 | /** 19 | * Denotes that a record has been deleted and the change persisted to the server. 20 | * 21 | * @property isDeleted 22 | * @type Boolean 23 | * @final 24 | * @for Model 25 | */ 26 | isDeleted: false, 27 | 28 | /** 29 | * Denotes that the record is currently saving its changes to the server, but the server hasn't responded yet. 30 | * (This doesn't overlap with `isCreating` at all. This is only true on subsequent saves.) 31 | * 32 | * @property isSaving 33 | * @type Boolean 34 | * @final 35 | * @for Model 36 | */ 37 | isSaving: false, 38 | 39 | /** 40 | * Denotes that the record is being reloaded from the server, but the server hasn't responded yet. 41 | * 42 | * @property isReloading 43 | * @type Boolean 44 | * @final 45 | * @for Model 46 | */ 47 | isReloading: false, 48 | 49 | /** 50 | * Denotes that a record has been loaded into a store and isn't freestanding. 51 | * 52 | * @property isLoaded 53 | * @type Boolean 54 | * @final 55 | * @for Model 56 | */ 57 | isLoaded: computed('store', { 58 | get() { 59 | return (this.get('store') !== null); 60 | } 61 | }), 62 | 63 | /** 64 | * Denotes that the record has attribute or relationship changes that have not been saved to the server yet. 65 | * Note: A new record is always dirty. 66 | * 67 | * @property isDirty 68 | * @type Boolean 69 | * @final 70 | * @for Model 71 | */ 72 | isDirty: Ember.computed.or('areAttributesDirty', 'areRelationshipsDirty', 'isNew'), 73 | 74 | /** 75 | * Denotes that the record is currently being saved to the server for the first time, 76 | * and the server hasn't responded yet. 77 | * 78 | * @property isCreating 79 | * @type Boolean 80 | * @final 81 | * @for Model 82 | */ 83 | isCreating: false, 84 | 85 | /** 86 | * Denotes that a record has just been created and has not been saved to 87 | * the server yet. Most likely has a temporary ID if this is true. 88 | * 89 | * @property isNew 90 | * @type Boolean 91 | * @final 92 | * @for Model 93 | */ 94 | isNew: computed('_id', { 95 | get() { 96 | return startsWith(this.get('_id'), this.constructor.temporaryIdPrefix); 97 | } 98 | }), 99 | 100 | /** 101 | * Denotes that the record is currently waiting for the server to respond to an operation. 102 | * 103 | * @property isInTransit 104 | * @type Boolean 105 | * @final 106 | * @for Model 107 | */ 108 | isInTransit: Ember.computed.or('isSaving', 'isDeleting', 'isCreating', 'isReloading') 109 | }; -------------------------------------------------------------------------------- /test/store/delete.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('Store Delete Test', { 7 | setup: function() { 8 | store = setupStore({ 9 | user: EG.Model.extend({ 10 | posts: EG.hasMany({ 11 | relatedType: 'post', 12 | inverse: 'author', 13 | isRequired: false 14 | }) 15 | }), 16 | 17 | post: EG.Model.extend({ 18 | author: EG.hasOne({ 19 | relatedType: 'user', 20 | inverse: 'posts', 21 | isRequired: false 22 | }), 23 | 24 | tags: EG.hasMany({ 25 | relatedType: 'tag', 26 | inverse: null, 27 | isRequired: false, 28 | defaultValue: function() { 29 | return ['0']; 30 | } 31 | }) 32 | }), 33 | 34 | tag: EG.Model.extend() 35 | }, { 36 | adapter: EG.Adapter.extend({ 37 | deleteRecord: function(record) { 38 | return Em.RSVP.Promise.resolve({}); 39 | } 40 | }) 41 | }); 42 | 43 | store.pushPayload({ 44 | user: [{ 45 | id: '1', 46 | posts: [ 47 | { type: 'post', id: '1' }, 48 | { type: 'post', id: '2' }, 49 | { type: 'post', id: '3' } 50 | ] 51 | }], 52 | post: [ 53 | { id: '1', author: { type: 'user', id: '1' }, tags: [] }, 54 | { id: '2', author: { type: 'user', id: '1' }, tags: [] }, 55 | { id: '3', author: { type: 'user', id: '1' }, tags: [] } 56 | ] 57 | }); 58 | } 59 | }); 60 | 61 | asyncTest('Relationships are properly disconnected on delete', function() { 62 | expect(6); 63 | 64 | var post1 = store.getRecord('post', '1'); 65 | var post2 = store.getRecord('post', '2'); 66 | var post3 = store.getRecord('post', '3'); 67 | 68 | start(); 69 | deepEqual(post1.get('_author'), { type: 'user', id: '1' }); 70 | deepEqual(post2.get('_author'), { type: 'user', id: '1' }); 71 | deepEqual(post3.get('_author'), { type: 'user', id: '1' }); 72 | stop(); 73 | 74 | store.getRecord('user', '1').destroy().then(function() { 75 | start(); 76 | 77 | strictEqual(post1.get('_author'), null); 78 | strictEqual(post2.get('_author'), null); 79 | strictEqual(post3.get('_author'), null); 80 | }); 81 | }); 82 | 83 | asyncTest('Delete new record', function() { 84 | expect(3); 85 | 86 | var user = store.getRecord('user', '1'); 87 | var post = store.createRecord('post', { 88 | author: '1' 89 | }); 90 | var id = post.get('id'); 91 | 92 | start(); 93 | ok(user.get('_posts').toArray().mapBy('id').indexOf(id) >= 0); 94 | stop(); 95 | 96 | post.destroy().then(function() { 97 | start(); 98 | 99 | strictEqual(post.get('store'), null); 100 | ok(user.get('_posts').toArray().indexOf(id) < 0); 101 | }); 102 | }); 103 | 104 | test('Delete records with `deletedRecords` meta attribute', function() { 105 | expect(2); 106 | 107 | var user = store.getRecord('user', '1'); 108 | deepEqual(user.get('_posts').mapBy('id').sort(), ['1', '2', '3'].sort()); 109 | 110 | store.pushPayload({ 111 | meta: { 112 | serverMeta: { 113 | deletedRecords: [ 114 | { type: 'post', id: '1' }, 115 | { type: 'post', id: '2' }, 116 | { type: 'post', id: '3' } 117 | ] 118 | } 119 | } 120 | }); 121 | 122 | deepEqual(user.get('_posts'), []); 123 | }); 124 | })(); -------------------------------------------------------------------------------- /src/util/data_adapter.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Model from 'ember-graph/model/model'; 3 | 4 | import { computed } from 'ember-graph/util/computed'; 5 | 6 | 7 | /** 8 | * Extends Ember's `DataAdapter` class to provide debug functionality for the Ember Inspector. 9 | * 10 | * Thanks to the Ember-Data team for the reference implementation. 11 | * 12 | * @class DataAdapter 13 | * @private 14 | */ 15 | const EmberGraphDataAdapter = Ember.DataAdapter && Ember.DataAdapter.extend({ 16 | 17 | containerDebugAdapter: computed({ 18 | get() { 19 | return Ember.getOwner(this).lookup('container-debug-adapter:main'); 20 | } 21 | }), 22 | 23 | getFilters: function() { 24 | return [ 25 | { name: 'isNew', desc: 'New' }, 26 | { name: 'isModified', desc: 'Modified' }, 27 | { name: 'isClean', desc: 'Clean' } 28 | ]; 29 | }, 30 | 31 | detect: function(modelClass) { 32 | return (modelClass !== Model && Model.detect(modelClass)); 33 | }, 34 | 35 | columnsForType: function(modelClass) { 36 | var attributeLimit = this.get('attributeLimit'); 37 | var columns = [{ name: 'id', desc: 'Id' }]; 38 | 39 | modelClass.eachAttribute(function(name, meta) { 40 | if (columns.length > attributeLimit) { 41 | return; 42 | } 43 | 44 | var desc = Ember.String.capitalize(Ember.String.underscore(name).replace(/_/g, ' ')); 45 | columns.push({ name: name, desc: desc }); 46 | }); 47 | 48 | return columns; 49 | }, 50 | 51 | getRecords: function(modelClass) { 52 | var typeKey = Ember.get(modelClass, 'typeKey'); 53 | return this.get('store').getLiveRecordArray(typeKey); 54 | }, 55 | 56 | getRecordColumnValues: function(record) { 57 | var values = { id: record.get('id') }; 58 | 59 | record.constructor.eachAttribute(function(name, meta) { 60 | values[name] = record.get(name); 61 | }); 62 | 63 | return values; 64 | }, 65 | 66 | getRecordKeywords: function(record) { 67 | var keywords = []; 68 | 69 | record.constructor.eachAttribute(function(name) { 70 | keywords.push(record.get(name) + ''); 71 | }); 72 | 73 | return keywords; 74 | }, 75 | 76 | getRecordFilterValues: function(record) { 77 | var isNew = record.get('isNew'); 78 | var isDirty = record.get('isDirty'); 79 | 80 | return { 81 | isNew: isNew, 82 | isModified: isDirty && !isNew, 83 | isClean: !isDirty 84 | }; 85 | }, 86 | 87 | getRecordColor: function(record) { 88 | if (record.get('isNew')) { 89 | return 'green'; 90 | } else if (record.get('isDirty')) { 91 | return 'blue'; 92 | } else { 93 | return 'black'; 94 | } 95 | }, 96 | 97 | observeRecord: function(record, recordUpdated) { 98 | var releaseMethods = Ember.A(); 99 | var propertiesToObserve = Ember.A(['id', 'isNew', 'isDirty']); 100 | 101 | propertiesToObserve.addObjects(Ember.get(record.constructor, 'attributes').toArray()); 102 | 103 | propertiesToObserve.forEach((name) => { 104 | var handler = () => this.wrapRecord(record); 105 | 106 | Ember.addObserver(record, name, handler); 107 | 108 | releaseMethods.push(() => Ember.removeObserver(record, name, handler)); 109 | }); 110 | 111 | return function() { 112 | releaseMethods.forEach((release) => release()); 113 | }; 114 | } 115 | }); 116 | 117 | const DataAdapter = (Ember.DataAdapter ? EmberGraphDataAdapter : null); 118 | 119 | export default DataAdapter; 120 | -------------------------------------------------------------------------------- /test/model/relationship/general.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | 6 | module('General Relationship Functionality', { 7 | setup: function() { 8 | store = setupStore({ 9 | user: EG.Model.extend({ 10 | vertices: EG.hasMany({ 11 | relatedType: 'vertex', 12 | isRequired: false, 13 | inverse: 'owner', 14 | defaultValue: [{ type: 'vertex', id: '0' }] 15 | }) 16 | }), 17 | 18 | vertex: EG.Model.extend({ 19 | owner: EG.hasOne({ 20 | relatedType: 'user', 21 | isRequired: true, 22 | inverse: 'vertices', 23 | readOnly: true 24 | }), 25 | 26 | tags: EG.hasMany({ 27 | relatedType: 'tag', 28 | isRequired: false, 29 | inverse: null 30 | }) 31 | }), 32 | 33 | tag: EG.Model.extend() 34 | }); 35 | 36 | store.pushPayload({ 37 | user: [ 38 | { 39 | id: '1', 40 | vertices: [ 41 | { type: 'vertex', id: '1' }, 42 | { type: 'vertex', id: '2' }, 43 | { type: 'vertex', id: '3' }, 44 | { type: 'vertex', id: '4' } 45 | ] 46 | }, 47 | { 48 | id: '2' 49 | } 50 | ], 51 | vertex: [ 52 | { 53 | id: '1', 54 | owner: { type: 'user', id: '1' }, 55 | tags: [ 56 | { type: 'tag', id: '1' }, 57 | { type: 'tag', id: '3' }, 58 | { type: 'tag', id: '5' } 59 | ] 60 | }, 61 | { 62 | id: '2', 63 | owner: { type: 'user', id: '1' }, 64 | tags: [ 65 | { type: 'tag', id: '2' }, 66 | { type: 'tag', id: '4' }, 67 | { type: 'tag', id: '6' } 68 | ] 69 | }, 70 | { 71 | id: '4', 72 | owner: { type: 'user', id: '3' } 73 | } 74 | ] 75 | }); 76 | } 77 | }); 78 | 79 | test('Loading a record with relationships works properly', function() { 80 | expect(3); 81 | 82 | var user = store.getRecord('user', '1'); 83 | deepEqual(user.get('_vertices').mapBy('id').sort(), ['1', '2', '3'].sort()); 84 | 85 | var vertex = store.getRecord('vertex', '2'); 86 | deepEqual(vertex.get('_tags').mapBy('id').sort(), ['2', '4', '6'].sort()); 87 | deepEqual(vertex.get('_owner'), { type: 'user', id: '1' }); 88 | }); 89 | 90 | test('Default relationship values are populated properly', function() { 91 | expect(2); 92 | 93 | var user = store.getRecord('user', '2'); 94 | deepEqual(user.get('_vertices'), [{ type: 'vertex', id: '0' }]); 95 | 96 | var vertex = store.getRecord('vertex', '4'); 97 | deepEqual(vertex.get('_tags'), []); 98 | }); 99 | 100 | test('Leaving out a required relationship causes an exception', function() { 101 | expect(1); 102 | 103 | throws(function() { 104 | store.pushPayload({ 105 | vertex: [{ id: '-1' }] 106 | }); 107 | }); 108 | }); 109 | 110 | test('Changing a read-only relationship causes an exception', function() { 111 | expect(1); 112 | 113 | var vertex = store.getRecord('vertex', '4'); 114 | 115 | throws(function() { 116 | vertex.clearHasOneRelationship('owner'); 117 | }); 118 | }); 119 | 120 | test('Changing a read-only relationship on a new record succeeds', function() { 121 | expect(1); 122 | 123 | var vertex = store.createRecord('vertex'); 124 | vertex.setHasOneRelationship('owner', '2'); 125 | deepEqual(vertex.get('_owner'), { type: 'user', id: '2' }); 126 | }); 127 | })(); -------------------------------------------------------------------------------- /src/attribute_type/enum.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraphSet from 'ember-graph/util/set'; 3 | import AttributeType from 'ember-graph/attribute_type/type'; 4 | 5 | import { computed } from 'ember-graph/util/computed'; 6 | 7 | 8 | /** 9 | * Represents an enumeration or multiple choice type. This class cannot be 10 | * instantiated directly, you must extend the class, overriding both the 11 | * `defaultValue` and `values` properties. The `values` property must be 12 | * an array of unique strings (case insensitive). The `defaultValue` must 13 | * be a string, and the value must also exist in the `values` array. 14 | * 15 | * @class EnumType 16 | * @extends AttributeType 17 | * @constructor 18 | */ 19 | export default AttributeType.extend({ 20 | 21 | /** 22 | * The default enum value. Must be overridden in subclasses. 23 | * 24 | * @property defaultValue 25 | * @type String 26 | * @final 27 | */ 28 | defaultValue: computed({ 29 | get() { 30 | throw new Ember.Error('You must override the `defaultValue` in an enumeration type.'); 31 | } 32 | }), 33 | 34 | /** 35 | * @property values 36 | * @type {Array} 37 | * @default [] 38 | * @final 39 | */ 40 | values: [], 41 | 42 | /** 43 | * Contains all of the values converted to lower case. 44 | * 45 | * @property valueSet 46 | * @type {Set} 47 | * @default [] 48 | * @final 49 | */ 50 | valueSet: computed('values', { 51 | get() { 52 | const set = EmberGraphSet.create(); 53 | const values = this.get('values'); 54 | 55 | set.addObjects(values.map((value) => value.toLocaleLowerCase())); 56 | 57 | return set; 58 | } 59 | }), 60 | 61 | /** 62 | * Determines if the given option is a valid enum value. 63 | * 64 | * @property isValidValue 65 | * @param {String} option 66 | * @return {Boolean} 67 | */ 68 | isValidValue: function(option) { 69 | return this.get('valueSet').contains(option.toLowerCase()); 70 | }, 71 | 72 | /** 73 | * Converts the given option to a valid enum value. 74 | * If the given value isn't valid, it uses the default value. 75 | * 76 | * @method serialize 77 | * @param {String} option 78 | * @return {String} 79 | */ 80 | serialize: function(option) { 81 | const optionString = option + ''; 82 | 83 | if (this.isValidValue(optionString)) { 84 | return optionString; 85 | } else { 86 | var defaultValue = this.get('defaultValue'); 87 | 88 | if (this.isValidValue(defaultValue)) { 89 | return defaultValue; 90 | } else { 91 | throw new Ember.Error('The default value you provided isn\'t a valid value.'); 92 | } 93 | } 94 | }, 95 | 96 | /** 97 | * 98 | * Converts the given option to a valid enum value. 99 | * If the given value isn't valid, it uses the default value. 100 | * 101 | * @method deserialize 102 | * @param {String} option 103 | * @return {String} 104 | */ 105 | deserialize() { 106 | return this.serialize(...arguments); 107 | }, 108 | 109 | /** 110 | * Compares two enum values, case-insensitive. 111 | * @param {String} a 112 | * @param {String} b 113 | * @return {Boolean} 114 | */ 115 | isEqual: function(a, b) { 116 | if (Ember.typeOf(a) !== 'string' || Ember.typeOf(b) !== 'string') { 117 | return false; 118 | } else { 119 | return ((a + '').toLocaleLowerCase() === (b + '').toLocaleLowerCase()); 120 | } 121 | } 122 | }); 123 | -------------------------------------------------------------------------------- /src/data/promise_object.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { computed } from 'ember-graph/util/computed'; 4 | 5 | /** 6 | * Ember's ObjectProxy combined with the PromiseProxyMixin. 7 | * Acts as an object and proxies all properties to the 8 | * given promise when it resolves. 9 | * 10 | * @class PromiseObject 11 | * @extends ObjectProxy 12 | * @uses PromiseProxyMixin 13 | * @constructor 14 | */ 15 | var PromiseObject = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin); 16 | 17 | /** 18 | * Ember's ArrayProxy combined with the PromiseProxyMixin. 19 | * Acts as an array and proxies all properties to the 20 | * given promise when it resolves. 21 | * 22 | * @class PromiseArray 23 | * @extends ArrayProxy 24 | * @uses PromiseProxyMixin 25 | * @constructor 26 | */ 27 | var PromiseArray = Ember.ArrayProxy.extend(Ember.PromiseProxyMixin); 28 | 29 | /** 30 | * Acts just like `PromiseObject` only it's able to hold the 31 | * ID and typeKey of a model before it's resolved completely. 32 | * 33 | * ```js 34 | * var user = EG.ModelPromiseObject.create({ 35 | * promise: this.store.find('user', '52'), 36 | * id: '52', 37 | * typeKey: 'user' 38 | * }); 39 | * 40 | * user.get('isPending'); // true 41 | * user.get('id'); // '52' 42 | * user.get('typeKey'); // 'user' 43 | * ``` 44 | * 45 | * @class ModelPromiseObject 46 | * @extends PromiseObject 47 | * @constructor 48 | */ 49 | var ModelPromiseObject = PromiseObject.extend({ 50 | __modelId: null, 51 | __modelTypeKey: null, 52 | 53 | id: computed('__modelId', 'content.id', { 54 | get() { 55 | const content = this.get('content'); 56 | 57 | if (content && content.get) { 58 | return content.get('id'); 59 | } else { 60 | return this.get('__modelId'); 61 | } 62 | }, 63 | set(key, value) { 64 | const content = this.get('content'); 65 | 66 | if (content && content.set) { 67 | content.set('id', value); 68 | } else { 69 | this.set('__modelId', value); 70 | } 71 | } 72 | }), 73 | 74 | typeKey: computed('__modelTypeKey', 'content.typeKey', { 75 | get() { 76 | const content = this.get('content'); 77 | 78 | if (content && content.get) { 79 | return content.get('typeKey'); 80 | } else { 81 | return this.get('__modelTypeKey'); 82 | } 83 | }, 84 | set(key, value) { 85 | const content = this.get('content'); 86 | 87 | if (content && content.set) { 88 | content.set('typeKey', value); 89 | } else { 90 | this.set('__modelTypeKey', value); 91 | } 92 | } 93 | }), 94 | 95 | /** 96 | * Returns the underlying model for this promise. If the promise 97 | * isn't resolved yet, the model will be `undefined`. 98 | * 99 | * @method getModel 100 | * @return {Model} 101 | */ 102 | getModel: function() { 103 | return this.get('content'); 104 | }, 105 | 106 | /** 107 | * Proxies to the underlying model's `destroy` method. 108 | * Will return a rejected promise if the promise isn't resolved yet. 109 | * 110 | * @method destroy 111 | * @return {Promise} 112 | */ 113 | destroy: function() { 114 | var model = this.getModel(); 115 | 116 | if (model && typeof model.destroy === 'function') { 117 | return model.destroy(); 118 | } else { 119 | return Ember.RSVP.Promise.reject('Can\'t destroy a record that is still loading.'); 120 | } 121 | } 122 | }); 123 | 124 | export { 125 | PromiseObject, 126 | PromiseArray, 127 | ModelPromiseObject 128 | }; -------------------------------------------------------------------------------- /src/model/relationships.md: -------------------------------------------------------------------------------- 1 | # Relationships 2 | 3 | The problem of maintaining relationship values from both the client and server is not an easy one. For me personally, 4 | it's probably the most difficult problem I've faced in my programming career. To help demystify some of what is going 5 | on, and to document my thought process for other people, I'm going to jot down notes here. They should hopefully 6 | clear up any decisions I've made and show the reasoning behind them. 7 | 8 | ## Relationship States 9 | 10 | From what I can tell, relationships can exist in three different states: 11 | 12 | - **Saved** - This is a relationship that has been saved to the server and hasn't been modified in any way on the 13 | client. It represents a current value and can't be deleted without the server's permission. 14 | - **Deleted** - This is a saved relationship that has been queued for deletion on the client. The server doesn't know 15 | that it has been queued for deletion until the client saves this change. This is a client-side relationship in the 16 | sense that only a single client can know about it until it changes state. But it's also a server-side relationship 17 | in the sense that the server may freely apply the change without the client's permission.Note 1 18 | - **Client** - This is a relationship that has just been created on a client. Again, the server doesn't know about it. 19 | This is a (kind of) hybrid relationship in the sense that both the server and client can change its state if need 20 | be. The server may upgrade this to a saved relationship if it wants to. Note 2 The server may also delete 21 | the relationship if server-client conflicts are set to side with the server. On the other hand, the client may also 22 | specifically request that the change be applied. If the client wants to remove this relationship, the relationship 23 | is destroyed completely; it doesn't have to be moved to an intermediate state first. 24 | 25 | ## Relationship Objects 26 | 27 | Ember-Graph uses `Relationship` objects to keep track of relationships. Initially, I wanted to keep relationships as 28 | properties on the model, but that turned out to not be such a great idea. Among several reasons, the most important 29 | is that relationships _should_ be separate entities. If you've ever studied graph theory, you'll know that nodes 30 | and edges are both equally important. 31 | 32 | As a consequence, I created the `Relationship` class. There are 7 main pieces of information in each relationship: 33 | 34 | - `type1` and `id1` are the type and ID for the record at the start of the relationship. And vice-versa, 35 | `type2` and `id2` are the type and ID for the record at the end of the relationship. 36 | - `relationship1` is the name of the relationship on record 1 that it belongs to. For instance, `relationship1` could 37 | be `'posts'` if the relationship connected a `user` and `post` object. `relationship2` is the opposite. 38 | - `state` is the current state of the relationship, which is one of these three: `client`, `server`, `deleted` 39 | 40 | Relationship objects are managed by the store, **not** by any particular record. Models have helper functions to 41 | make manipulating relationships easy, but in the end, only the store can create, modify or delete a relationship. 42 | Most people see relationships as properties of records, but they're not. Relationships are objects that, 43 | when combined with records, create a complete graph. In order to maintain this graph, and its consistency, there 44 | must be a single source of truth and a single manager. For Ember-Graph, I chose this to be the store (for now). -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "reset": true, 3 | 4 | "rules": { 5 | // Possible errors 6 | "comma-dangle": [2, "never"], 7 | "no-cond-assign": [2, "always"], 8 | "no-console": 2, 9 | "no-constant-condition": 2, 10 | "no-control-regex": 2, 11 | "no-debugger": 2, 12 | "no-dupe-args": 2, 13 | "no-dupe-keys": 2, 14 | "no-duplicate-case": 2, 15 | "no-empty-character-class": 2, 16 | "no-empty": 2, 17 | "no-ex-assign": 2, 18 | "no-extra-boolean-cast": 2, 19 | "no-extra-semi": 2, 20 | "no-func-assign": 2, 21 | "no-inner-declarations": 2, 22 | "no-invalid-regexp": 2, 23 | "no-irregular-whitespace": 2, 24 | "no-negated-in-lhs": 2, 25 | "no-obj-calls": 2, 26 | "no-regex-spaces": 2, 27 | "no-sparse-arrays": 2, 28 | "no-unreachable": 2, 29 | "use-isnan": 2, 30 | "valid-typeof": 2, 31 | "no-unexpected-multiline": 2, 32 | 33 | // Best practices 34 | "curly": 2, 35 | "dot-location": [2, "object"], 36 | "eqeqeq": 2, 37 | "guard-for-in": 2, 38 | "no-alert": 2, 39 | "no-caller": 2, 40 | "no-div-regex": 2, 41 | "no-empty-label": 2, 42 | "no-eq-null": 2, 43 | "no-eval": 2, 44 | "no-extra-bind": 2, 45 | "no-fallthrough": 2, 46 | "no-floating-decimal": 2, 47 | "no-implied-eval": 2, 48 | "no-iterator": 2, 49 | "no-labels": 2, 50 | "no-lone-blocks": 2, 51 | "no-loop-func": 2, 52 | "no-multi-str": 2, 53 | "no-native-reassign": 2, 54 | "no-new-func": 2, 55 | "no-new-wrappers": 2, 56 | "no-new": 2, 57 | "no-octal": 2, 58 | // To be enabled over time 59 | //"no-param-reassign": 2, 60 | "no-process-env": 2, 61 | "no-proto": 2, 62 | "no-redeclare": 2, 63 | "no-return-assign": 2, 64 | "no-script-url": 2, 65 | "no-self-compare": 2, 66 | "no-sequences": 2, 67 | "no-unused-expressions": 2, 68 | "no-useless-call": 2, 69 | "no-useless-concat": 2, 70 | "no-void": 2, 71 | "no-with": 2, 72 | "radix": 2, 73 | "yoda": [2, "never"], 74 | 75 | // Variables 76 | // To be enabled over time 77 | //"init-declarations": [2, "always"], 78 | "no-catch-shadow": 2, 79 | "no-delete-var": 2, 80 | "no-label-var": 2, 81 | "no-shadow-restricted-names": 2, 82 | "no-undef-init": 2, 83 | "no-undef": 2, 84 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 85 | "no-use-before-define": 2, 86 | 87 | // Stylistic Issues 88 | "array-bracket-spacing": [2, "never"], 89 | "block-spacing": [2, "always"], 90 | "brace-style": [2, "1tbs"], 91 | "camelcase": 2, 92 | "comma-spacing": [2, { "before": false, "after": true }], 93 | "comma-style": [2, "last"], 94 | "computed-property-spacing": [2, "never"], 95 | // To be enabled over time 96 | //"func-style": [2, "expression"], 97 | "indent": [2, "tab", { "SwitchCase": 1 }], 98 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 99 | "linebreak-style": [2, "unix"], 100 | "new-cap": [2, { "capIsNewExceptions": ["Ember.A"] }], 101 | "new-parens": 2, 102 | "no-array-constructor": 2, 103 | "no-inline-comments": 2, 104 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 105 | "no-new-object": 2, 106 | "no-spaced-func": 2, 107 | "no-trailing-spaces": 2, 108 | "object-curly-spacing": [2, "always"], 109 | "one-var": [2, "never"], 110 | "quotes": [2, "single"], 111 | "semi": [2, "always"], 112 | "space-after-keywords": [2, "always"], 113 | "space-before-blocks": [2, "always"], 114 | "space-before-function-paren": [2, "never"], 115 | "space-in-parens": [2, "never"], 116 | "space-infix-ops": 2, 117 | "space-return-throw-case": 2, 118 | "spaced-comment": [2, "always"] 119 | 120 | } 121 | } -------------------------------------------------------------------------------- /tasks/build_api_pages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var Handlebars = require('handlebars'); 5 | 6 | var templates = {}; 7 | 8 | module.exports = function(grunt) { 9 | grunt.registerTask('build_api_pages', function() { 10 | compileTemplates(); 11 | 12 | var data = JSON.parse(fs.readFileSync('./doc/ember-graph.json', { encoding: 'utf8' })); 13 | buildNamespacePages(data); 14 | buildClassPages(data); 15 | }); 16 | }; 17 | 18 | function compileTemplates() { 19 | function getTemplate(name) { 20 | return fs.readFileSync('./site/templates/' + name + '.hbs', { encoding: 'utf8' }); 21 | } 22 | 23 | var names = ['api/content_index', 'api/content_methods', 'api/content_properties', 24 | 'api/content_tabs', 'api/shell', 'api/sidebar', 'api/base']; 25 | 26 | names.forEach(function(name) { 27 | var template = Handlebars.compile(getTemplate(name)); 28 | 29 | templates[name] = function(data) { 30 | return template(data).trim(); 31 | }; 32 | }); 33 | } 34 | 35 | function buildNamespacePages(data) { 36 | var namespaces = data.namespaces; 37 | var classNames = data.classes.map(function(c) { 38 | return c.name; 39 | }); 40 | var namespaceNames = data.namespaces.map(function(n) { 41 | return n.name; 42 | }); 43 | 44 | namespaces.forEach(function(namespace) { 45 | var index = templates['api/content_index']({ methods: namespace.methods, properties: namespace.properties }); 46 | var methodList = templates['api/content_methods']({ methods: namespace.methods }); 47 | var propertyList = templates['api/content_properties']({ properties: namespace.properties }); 48 | 49 | var tabs = templates['api/content_tabs']({ index: index, properties: propertyList, methods: methodList }); 50 | var sidebar = buildSidebar(classNames, namespaceNames, null, namespace.name); 51 | 52 | var page = templates['api/shell']({ 53 | sidebar: sidebar, 54 | content: tabs, 55 | namespace: { 56 | name: namespace.name, 57 | description: namespace.description 58 | } 59 | }); 60 | var file = templates['api/base']({ body: page }); 61 | 62 | fs.writeFileSync('site_build/api/' + namespace.name + '.html', file); 63 | }); 64 | } 65 | 66 | function buildClassPages(data) { 67 | var classes = data.classes; 68 | var classNames = data.classes.map(function(c) { 69 | return c.name; 70 | }); 71 | var namespaceNames = data.namespaces.map(function(n) { 72 | return n.name; 73 | }); 74 | 75 | classes.forEach(function(c) { 76 | var index = templates['api/content_index']({ methods: c.methods, properties: c.properties }); 77 | var methodList = templates['api/content_methods']({ methods: c.methods }); 78 | var propertyList = templates['api/content_properties']({ properties: c.properties }); 79 | 80 | var tabs = templates['api/content_tabs']({ index: index, properties: propertyList, methods: methodList }); 81 | var sidebar = buildSidebar(classNames, namespaceNames, c.name, null); 82 | 83 | var page = templates['api/shell']({ sidebar: sidebar, content: tabs, 'class': c }); 84 | var file = templates['api/base']({ body: page }); 85 | 86 | fs.writeFileSync('site_build/api/' + c.name + '.html', file); 87 | }); 88 | } 89 | 90 | function buildSidebar(classNames, namespaceNames, currentClass, currentNamespace) { 91 | var classes = classNames.map(function(className) { 92 | return { 93 | name: className, 94 | active: className === currentClass 95 | }; 96 | }); 97 | 98 | var namespaces = namespaceNames.map(function(namespace) { 99 | return { 100 | name: namespace, 101 | active: currentNamespace === namespace 102 | }; 103 | }); 104 | 105 | return templates['api/sidebar']({ 106 | classes: classes, 107 | namespaces: namespaces 108 | }); 109 | } -------------------------------------------------------------------------------- /test/relationship/relationship_store.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var CLIENT_STATE = EG.Relationship.CLIENT_STATE; 5 | var SERVER_STATE = EG.Relationship.SERVER_STATE; 6 | var DELETED_STATE = EG.Relationship.DELETED_STATE; 7 | 8 | var Relationship = EG.Relationship; 9 | 10 | var store; 11 | 12 | module('Relationship Store Test', { 13 | setup: function() { 14 | store = EG.RelationshipStore.create(); 15 | } 16 | }); 17 | 18 | test('Add and fetch relationships', function() { 19 | expect(14); 20 | 21 | deepEqual(store.getCurrentRelationships('posts'), []); 22 | deepEqual(store.getServerRelationships('posts'), []); 23 | deepEqual(store.getCurrentRelationships('author'), []); 24 | deepEqual(store.getServerRelationships('author'), []); 25 | 26 | strictEqual(store.get('server.length'), 0); 27 | strictEqual(store.get('client.length'), 0); 28 | strictEqual(store.get('deleted.length'), 0); 29 | 30 | var r1 = Relationship.create('user', '1', 'posts', 'post', '1', 'author', SERVER_STATE); 31 | var r2 = Relationship.create('user', '1', 'posts', 'post', '2', 'author', SERVER_STATE); 32 | var r3 = Relationship.create('user', '1', 'posts', 'post', '3', 'author', DELETED_STATE); 33 | var r4 = Relationship.create('user', '1', 'posts', 'post', '4', 'author', CLIENT_STATE); 34 | var r5 = Relationship.create('user', '1', 'posts', 'post', '5', 'author', CLIENT_STATE); 35 | 36 | store.addRelationship('posts', r1); 37 | store.addRelationship('posts', r2); 38 | store.addRelationship('posts', r3); 39 | store.addRelationship('posts', r4); 40 | store.addRelationship('posts', r5); 41 | 42 | deepEqual(store.getCurrentRelationships('posts').mapBy('id').sort(), [r1, r2, r4, r5].mapBy('id').sort()); 43 | deepEqual(store.getServerRelationships('posts').mapBy('id').sort(), [r1, r2, r3].mapBy('id').sort()); 44 | deepEqual(store.getCurrentRelationships('author'), []); 45 | deepEqual(store.getServerRelationships('author'), []); 46 | 47 | strictEqual(store.get('server.length'), 2); 48 | strictEqual(store.get('client.length'), 2); 49 | strictEqual(store.get('deleted.length'), 1); 50 | }); 51 | 52 | test('Remove relationships', function() { 53 | expect(13); 54 | 55 | var r1 = Relationship.create('user', '1', 'posts', 'post', '1', 'author', SERVER_STATE); 56 | var r2 = Relationship.create('user', '1', 'posts', 'post', '2', 'author', SERVER_STATE); 57 | var r3 = Relationship.create('user', '1', 'posts', 'post', '3', 'author', DELETED_STATE); 58 | var r4 = Relationship.create('user', '1', 'posts', 'post', '4', 'author', CLIENT_STATE); 59 | var r5 = Relationship.create('user', '1', 'posts', 'post', '5', 'author', CLIENT_STATE); 60 | 61 | store.addRelationship('posts', r1); 62 | store.addRelationship('posts', r2); 63 | store.addRelationship('posts', r3); 64 | store.addRelationship('posts', r4); 65 | store.addRelationship('posts', r5); 66 | 67 | strictEqual(store.get('server.length'), 2); 68 | strictEqual(store.get('client.length'), 2); 69 | strictEqual(store.get('deleted.length'), 1); 70 | 71 | store.removeRelationship(r1); 72 | store.removeRelationship(r4); 73 | 74 | strictEqual(store.get('server.length'), 1); 75 | strictEqual(store.get('client.length'), 1); 76 | strictEqual(store.get('deleted.length'), 1); 77 | 78 | deepEqual(store.getCurrentRelationships('posts').mapBy('id').sort(), [r2, r5].mapBy('id').sort()); 79 | deepEqual(store.getServerRelationships('posts').mapBy('id').sort(), [r2, r3].mapBy('id').sort()); 80 | 81 | store.clearRelationships('posts'); 82 | 83 | deepEqual(store.getCurrentRelationships('posts'), []); 84 | deepEqual(store.getServerRelationships('posts'), []); 85 | 86 | strictEqual(store.get('server.length'), 0); 87 | strictEqual(store.get('client.length'), 0); 88 | strictEqual(store.get('deleted.length'), 0); 89 | }); 90 | })(); -------------------------------------------------------------------------------- /test/store/unload.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | var Promise = Em.RSVP.Promise; 6 | 7 | module('Store Unload Test', { 8 | setup: function() { 9 | store = setupStore({ 10 | user: EG.Model.extend({ 11 | email: EG.attr({ 12 | type: 'string', 13 | isRequired: true, 14 | readOnly: true 15 | }), 16 | 17 | posts: EG.hasMany({ 18 | relatedType: 'post', 19 | inverse: 'author', 20 | isRequired: true 21 | }) 22 | }), 23 | 24 | post: EG.Model.extend({ 25 | title: EG.attr({ 26 | type: 'string', 27 | isRequired: true, 28 | readOnly: true 29 | }), 30 | 31 | body: EG.attr({ 32 | type: 'string', 33 | isRequired: false, 34 | defaultValue: '' 35 | }), 36 | 37 | author: EG.hasOne({ 38 | relatedType: 'user', 39 | inverse: 'posts', 40 | isRequired: true 41 | }), 42 | 43 | tags: EG.hasMany({ 44 | relatedType: 'tag', 45 | inverse: null, 46 | isRequired: false 47 | }) 48 | }), 49 | 50 | tag: EG.Model.extend({ 51 | name: EG.attr({ 52 | type: 'string', 53 | isRequired: true, 54 | readOnly: true 55 | }) 56 | }), 57 | 58 | item: EG.Model.extend({ 59 | name: EG.attr({ 60 | type: 'string' 61 | }) 62 | }) 63 | }, { 64 | adapter: EG.Adapter.extend({ 65 | updateRecord: function(record) { 66 | return Promise.resolve(); 67 | } 68 | }) 69 | }); 70 | } 71 | }); 72 | 73 | test('Unload record with no relationships', function() { 74 | expect(3); 75 | 76 | store.pushPayload({ 77 | item: [{ id: '1', name: 'First' }] 78 | }); 79 | 80 | var record = store.getRecord('item', '1'); 81 | 82 | strictEqual(record.get('id'), '1'); 83 | strictEqual(record.get('name'), 'First'); 84 | 85 | store.unloadRecord(record); 86 | 87 | strictEqual(store.getRecord('item', '1'), null); 88 | }); 89 | 90 | test('Unload record with relationships', function() { 91 | expect(6); 92 | 93 | store.pushPayload({ 94 | user: [{ 95 | id: '1', 96 | email: 'test@test.com', 97 | posts: [ 98 | { type: 'post', id: '1' }, 99 | { type: 'post', id: '2' } 100 | ] 101 | }], 102 | post: [ 103 | { id: '1', title: 'One', author: { type: 'user', id: '1' } }, 104 | { id: '2', title: 'Two', author: { type: 'user', id: '1' } } 105 | ] 106 | }); 107 | 108 | var user = store.getRecord('user', '1'); 109 | var post1 = store.getRecord('post', '1'); 110 | var post2 = store.getRecord('post', '2'); 111 | 112 | deepEqual(user.get('_posts').mapBy('id').sort(), ['1', '2'].sort()); 113 | deepEqual(post1.get('_author'), { type: 'user', id: '1' }); 114 | deepEqual(post2.get('_author'), { type: 'user', id: '1' }); 115 | 116 | store.unloadRecord(user); 117 | strictEqual(store.getRecord('user', '1'), null); 118 | deepEqual(post1.get('_author'), { type: 'user', id: '1' }); 119 | deepEqual(post2.get('_author'), { type: 'user', id: '1' }); 120 | }); 121 | 122 | test('Unloading dirty record throws error', function() { 123 | expect(1); 124 | 125 | store.pushPayload({ 126 | item: [{ id: '1', name: 'First' }] 127 | }); 128 | 129 | var item = store.getRecord('item', '1'); 130 | item.set('name', ''); 131 | 132 | throws(function() { 133 | store.unloadRecord(item); 134 | }); 135 | }); 136 | 137 | test('Unload dirty record by telling store to clear changes', function() { 138 | expect(1); 139 | 140 | store.pushPayload({ 141 | item: [{ id: '1', name: 'First' }] 142 | }); 143 | 144 | var item = store.getRecord('item', '1'); 145 | item.set('name', ''); 146 | store.unloadRecord(item, true); 147 | 148 | strictEqual(store.getRecord('item', '1'), null); 149 | }); 150 | })(); 151 | -------------------------------------------------------------------------------- /test/store/save.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var store; 5 | var Promise = Em.RSVP.Promise; 6 | 7 | module('Store Test', { 8 | setup: function() { 9 | store = setupStore({ 10 | user: EG.Model.extend({ 11 | email: EG.attr({ 12 | type: 'string', 13 | isRequired: true, 14 | readOnly: true 15 | }), 16 | 17 | posts: EG.hasMany({ 18 | relatedType: 'post', 19 | inverse: 'author', 20 | isRequired: true 21 | }) 22 | }), 23 | 24 | post: EG.Model.extend({ 25 | title: EG.attr({ 26 | type: 'string', 27 | isRequired: true, 28 | readOnly: true 29 | }), 30 | 31 | body: EG.attr({ 32 | type: 'string', 33 | isRequired: false, 34 | defaultValue: '' 35 | }), 36 | 37 | author: EG.hasOne({ 38 | relatedType: 'user', 39 | inverse: 'posts', 40 | isRequired: true 41 | }), 42 | 43 | tags: EG.hasMany({ 44 | relatedType: 'tag', 45 | inverse: null, 46 | isRequired: true 47 | }) 48 | }), 49 | 50 | tag: EG.Model.extend({ 51 | name: EG.attr({ 52 | type: 'string', 53 | isRequired: true, 54 | readOnly: true 55 | }) 56 | }) 57 | }, { 58 | adapter: EG.Adapter.extend({ 59 | updateRecord: function(record) { 60 | return Em.RSVP.Promise.resolve(); 61 | } 62 | }) 63 | }); 64 | } 65 | }); 66 | 67 | asyncTest('Can create record without `createdRecord` meta attribute', function() { 68 | expect(5); 69 | 70 | var tag = store.createRecord('tag', { name: 'tag1' }); 71 | 72 | start(); 73 | strictEqual(tag.get('name'), 'tag1'); 74 | strictEqual(tag.get('isNew'), true); 75 | stop(); 76 | 77 | store.adapterFor('application').createRecord = function() { 78 | return Promise.resolve({ tag: [{ id: '100', name: 'tag1' }] }); 79 | }; 80 | 81 | tag.save().then(function() { 82 | start(); 83 | strictEqual(tag.get('id'), '100'); 84 | strictEqual(tag.get('name'), 'tag1'); 85 | strictEqual(tag.get('isNew'), false); 86 | }); 87 | }); 88 | 89 | asyncTest('Excluding `createdRecord` throws when more than one record of a type is included', function() { 90 | expect(1); 91 | 92 | store.adapterFor('application').createRecord = function() { 93 | return Promise.resolve({ tag: [{}, {}, {}] }); 94 | }; 95 | 96 | store.createRecord('tag', { name: '' }).save().catch(function(error) { 97 | start(); 98 | ok(error.message.indexOf('`createdRecord`') >= 0); 99 | }); 100 | }); 101 | 102 | asyncTest('Saving without returning a payload updates the record', function() { 103 | expect(10); 104 | 105 | store.pushPayload({ 106 | user: [{ 107 | id: '1', 108 | email: 'test@test', 109 | posts: [] 110 | }], 111 | post: [{ 112 | id: '1', 113 | title: '', 114 | body: '', 115 | author: null, 116 | tags: [] 117 | }], 118 | tag: [{ 119 | id: '1', 120 | name: 'tag1' 121 | }] 122 | }); 123 | 124 | var post = store.getRecord('post', '1'); 125 | post.set('body', 'Test Body'); 126 | post.setHasOneRelationship('author', '1'); 127 | post.addToRelationship('tags', '1'); 128 | 129 | start(); 130 | strictEqual(post.get('title'), ''); 131 | strictEqual(post.get('body'), 'Test Body'); 132 | deepEqual(post.get('_author'), { type: 'user', id: '1' }); 133 | deepEqual(post.get('_tags'), [{ type: 'tag', id: '1' }]); 134 | strictEqual(post.get('isDirty'), true); 135 | stop(); 136 | 137 | post.save().then(function() { 138 | start(); 139 | 140 | strictEqual(post.get('title'), ''); 141 | strictEqual(post.get('body'), 'Test Body'); 142 | deepEqual(post.get('_author'), { type: 'user', id: '1' }); 143 | deepEqual(post.get('_tags'), [{ type: 'tag', id: '1' }]); 144 | strictEqual(post.get('isDirty'), false); 145 | }); 146 | }); 147 | })(); 148 | -------------------------------------------------------------------------------- /src/serializer/serializer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { abstractMethod } from 'ember-graph/util/util'; 4 | 5 | /** 6 | * An interface for a serializer. A serializer is used to convert 7 | * objects back and forth between the JSON that the server uses, 8 | * and the records that are used on the client side. 9 | * 10 | * @class Serializer 11 | */ 12 | export default Ember.Object.extend({ 13 | 14 | /** 15 | * The store that the records will be loaded into. This 16 | * property is injected by the container on startup. 17 | * This can be used for fetching models and their metadata. 18 | * 19 | * @property store 20 | * @type Store 21 | * @final 22 | */ 23 | store: null, 24 | 25 | /** 26 | * Converts a record to a JSON payload that can be sent to 27 | * the server. The options object is a general object where any options 28 | * necessary can be passed in from the adapter. The built-in Ember-Graph 29 | * adapters pass in just one option: `requestType`. This lets the 30 | * serializer know what kind of request will made using the payload 31 | * returned from this call. The value of `requestType` will be one of 32 | * either `createRecord` or `updateRecord`. If you write a custom 33 | * adapter or serializer, you're free to pass in any other options 34 | * you may need. 35 | * 36 | * @method serialize 37 | * @param {Model} record The record to serialize 38 | * @param {Object} [options] Any options that were passed by the adapter 39 | * @return {JSON} JSON payload to send to server 40 | * @abstract 41 | */ 42 | serialize: abstractMethod('serialize'), 43 | 44 | /** 45 | * Takes a payload from the server and converts it into a normalized 46 | * JSON payload that the store can use. Details about the format 47 | * can be found in the {{link-to-method 'Store' 'pushPayload'}} 48 | * documentation. 49 | * 50 | * In addition to the format described by the store, the store 51 | * may require some additional information. This information should 52 | * be included in the `meta` object. The attributes required by the 53 | * store are: 54 | * 55 | * - `matchedRecords`: This is an array of objects (with `type` and 56 | * `id` fields) that tell which records were matched on a query. 57 | * This helps distinguish queried records from records of the 58 | * same type that may have been side loaded. If this property 59 | * doesn't exist, the adapter will assume that all objects 60 | * of that type were returned by the query. 61 | * - `createdRecord`: This is a single object (with `type` and `id`) 62 | * that tells the adapter which record was created as the result 63 | * of a `createRecord` request. Again, this helps distinguish 64 | * the record from other records of the same type. 65 | * 66 | * To determine whether those meta attributes are required or not, the 67 | * `requestType` option can be used. The built-in Ember-Graph adapters 68 | * will pass one of the following values: `findRecord`, `findMany`, 69 | * `findAll`, `findQuery`, `createRecord`, `updateRecord`, `deleteRecord`. 70 | * If the value is `findQuery`, then the `matchedRecords` meta attribute is 71 | * required. If the value is `createRecord`, then the `createdRecord` meta 72 | * attribute is required. 73 | * 74 | * In addition to `requestType`, the following options are available: 75 | * 76 | * - `recordType`: The type of record that the request was performed on 77 | * - `id`: The ID of the record referred to by a `findRecord`, 78 | * `updateRecord` or `deleteRecord` request 79 | * - `ids`: The IDs of the records requested by a `findMany` request 80 | * - `query`: The query submitted to the `findQuery` request 81 | * 82 | * @method deserialize 83 | * @param {JSON} payload 84 | * @param {Object} [options] Any options that were passed by the adapter 85 | * @return {Object} Normalized JSON payload 86 | * @abstract 87 | */ 88 | deserialize: abstractMethod('deserialize') 89 | }); 90 | -------------------------------------------------------------------------------- /tasks/transpile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var babel = require('babel'); 6 | var readdir = require('fs-readdir-recursive'); 7 | 8 | var SOURCE_DIRECTORY = path.resolve(process.cwd() + '/src'); 9 | var DESTINATION_DIRECTORY = path.resolve(process.cwd() + '/dist'); 10 | var OUTPUT_FILE_PATH = path.resolve(process.cwd() + '/dist/ember-graph.js'); 11 | var BEFORE_LOAD_SCRIPT = path.resolve(process.cwd() + '/src/before_load.js'); 12 | var AFTER_LOAD_SCRIPT = path.resolve(process.cwd() + '/src/after_load.js'); 13 | 14 | var FILE_CACHE = {}; 15 | var LAST_TRANSPILE = 0; 16 | 17 | module.exports = function(grunt) { 18 | grunt.registerTask('transpile', transpile); 19 | }; 20 | 21 | function transpile() { 22 | console.log('Transpiling...'); 23 | var startTime = Date.now(); 24 | buildOutput(); 25 | LAST_TRANSPILE = Date.now(); 26 | var timeElapsed = LAST_TRANSPILE - startTime; 27 | console.log('...done. Finished in ' + Math.ceil(timeElapsed / 1000) + ' seconds'); 28 | } 29 | 30 | function buildOutput() { 31 | try { 32 | fs.mkdirSync(DESTINATION_DIRECTORY); 33 | } catch (e) { 34 | if (e.code !== 'EEXIST') { 35 | throw e; 36 | } 37 | } 38 | 39 | transpileSources(); 40 | cleanCache(); 41 | concatenateFiles(); 42 | } 43 | 44 | function transpileSources() { 45 | var filePaths = scanForFiles(); 46 | var changedFiles = filterUnchangedFiles(filePaths); 47 | changedFiles.forEach(function(filePath) { 48 | console.log('Transpiling file: ' + path.relative(SOURCE_DIRECTORY, filePath)); 49 | FILE_CACHE[filePath] = transpileFile(filePath); 50 | }); 51 | } 52 | 53 | function cleanCache() { 54 | Object.keys(FILE_CACHE).forEach(function(filePath) { 55 | if (!fs.existsSync(filePath)) { 56 | delete FILE_CACHE[filePath]; 57 | } 58 | }); 59 | } 60 | 61 | function concatenateFiles() { 62 | fs.writeFileSync(OUTPUT_FILE_PATH, '(function() {\n\'use strict\';\n\n'); 63 | 64 | var beforeLoadScriptContents = fs.readFileSync(BEFORE_LOAD_SCRIPT, 'utf8'); 65 | fs.appendFileSync(OUTPUT_FILE_PATH, beforeLoadScriptContents); 66 | 67 | Object.keys(FILE_CACHE).forEach(function(filePath) { 68 | fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n'); 69 | fs.appendFileSync(OUTPUT_FILE_PATH, FILE_CACHE[filePath]); 70 | }); 71 | 72 | fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n'); 73 | var afterLoadScriptContents = fs.readFileSync(AFTER_LOAD_SCRIPT, 'utf8'); 74 | fs.appendFileSync(OUTPUT_FILE_PATH, afterLoadScriptContents); 75 | 76 | fs.appendFileSync(OUTPUT_FILE_PATH, '\n\n}).call(window || this);'); 77 | } 78 | 79 | function scanForFiles() { 80 | return readdir(SOURCE_DIRECTORY).map(function(name) { 81 | return path.resolve(SOURCE_DIRECTORY + '/' + name); 82 | }).filter(function(name) { 83 | if (!endsWith(name, '.js')) { 84 | return false; 85 | } 86 | 87 | if (name === BEFORE_LOAD_SCRIPT) { 88 | return false; 89 | } 90 | 91 | if (name === AFTER_LOAD_SCRIPT) { 92 | return false; 93 | } 94 | 95 | return true; 96 | }); 97 | } 98 | 99 | function filterUnchangedFiles(filePaths) { 100 | return filePaths.filter(function(filePath) { 101 | var stat = fs.statSync(filePath); 102 | return (stat.mtime.getTime() > LAST_TRANSPILE); 103 | }); 104 | } 105 | 106 | function transpileFile(filePath) { 107 | var fileContents = fs.readFileSync(filePath, 'utf8'); 108 | var options = { 109 | filename: path.relative(SOURCE_DIRECTORY, filePath), 110 | nonStandard: false, 111 | sourceRoot: SOURCE_DIRECTORY, 112 | moduleRoot: 'ember-graph', 113 | modules: 'amdStrict', 114 | moduleIds: true, 115 | whitelist: [ 116 | 'es6.arrowFunctions', 117 | 'es6.blockScoping', 118 | 'es6.constants', 119 | 'es6.destructuring', 120 | 'es6.modules', 121 | 'es6.parameters', 122 | 'es6.properties.computed', 123 | 'es6.properties.shorthand', 124 | 'es6.spread', 125 | 'es6.templateLiterals' 126 | ] 127 | }; 128 | 129 | return babel.transform(fileContents, options).code; 130 | } 131 | 132 | function endsWith(string, suffix) { 133 | return string.indexOf(suffix, string.length - suffix.length) >= 0; 134 | } -------------------------------------------------------------------------------- /src/relationship/relationship.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { generateUUID } from 'ember-graph/util/util'; 4 | import { RelationshipStates } from 'ember-graph/constants'; 5 | import { computed } from 'ember-graph/util/computed'; 6 | 7 | var CLIENT_STATE = RelationshipStates.CLIENT_STATE; 8 | var SERVER_STATE = RelationshipStates.SERVER_STATE; 9 | var DELETED_STATE = RelationshipStates.DELETED_STATE; 10 | 11 | var Relationship = Ember.Object.extend({ 12 | 13 | _state: CLIENT_STATE, 14 | state: computed('_state', { 15 | get() { 16 | return this.get('_state'); 17 | }, 18 | set(key, value) { 19 | switch (value) { 20 | case CLIENT_STATE: 21 | case SERVER_STATE: 22 | case DELETED_STATE: 23 | this.set('_state', value); 24 | break; 25 | default: 26 | Ember.assert('Invalid relationship state: ' + value); 27 | break; 28 | } 29 | } 30 | }), 31 | 32 | id: null, 33 | 34 | type1: null, 35 | 36 | id1: null, 37 | 38 | relationship1: null, 39 | 40 | type2: null, 41 | 42 | id2: null, 43 | 44 | relationship2: null, 45 | 46 | isConnectedTo: function(record) { 47 | if (this.get('type1') === record.typeKey && this.get('id1') === record.get('id')) { 48 | return true; 49 | } 50 | 51 | if (this.get('type2') === record.typeKey && this.get('id2') === record.get('id')) { 52 | return true; 53 | } 54 | 55 | return false; 56 | }, 57 | 58 | matchesOneSide: function(type, id, name) { 59 | if (this.get('type1') === type && this.get('id1') === id && this.get('relationship1') === name) { 60 | return true; 61 | } 62 | 63 | if (this.get('type2') === type && this.get('id2') === id && this.get('relationship2') === name) { 64 | return true; 65 | } 66 | 67 | return false; 68 | }, 69 | 70 | otherType: function(record) { 71 | // If they have the same type, it won't matter which branch is taken 72 | if (this.get('type1') === record.typeKey) { 73 | return this.get('type2'); 74 | } else { 75 | return this.get('type1'); 76 | } 77 | }, 78 | 79 | otherId: function(record) { 80 | // If they have the same IDs, it won't matter which branch is taken 81 | if (this.get('id1') === record.get('id')) { 82 | return this.get('id2'); 83 | } else { 84 | return this.get('id1'); 85 | } 86 | }, 87 | 88 | otherName: function(record) { 89 | if (this.get('id1') === record.get('id') && this.get('type1') === record.typeKey) { 90 | return this.get('relationship2'); 91 | } else { 92 | return this.get('relationship1'); 93 | } 94 | }, 95 | 96 | thisName: function(record) { 97 | if (this.get('id1') === record.get('id') && this.get('type1') === record.typeKey) { 98 | return this.get('relationship1'); 99 | } else { 100 | return this.get('relationship2'); 101 | } 102 | }, 103 | 104 | changeId: function(typeKey, oldId, newId) { 105 | if (this.get('type1') === typeKey && this.get('id1') === oldId) { 106 | this.set('id1', newId); 107 | } else if (this.get('type2') === typeKey && this.get('id2') === oldId) { 108 | this.set('id2', newId); 109 | } 110 | }, 111 | 112 | erase: function() { 113 | this.setProperties({ 114 | id: null, 115 | type1: null, 116 | id1: null, 117 | relationship1: null, 118 | type2: null, 119 | id2: null, 120 | relationship2: null, 121 | _state: null 122 | }); 123 | } 124 | }); 125 | 126 | Relationship.reopenClass({ 127 | // TODO: NEW_STATE, SAVED_STATE, DELETED_STATE 128 | CLIENT_STATE: CLIENT_STATE, 129 | SERVER_STATE: SERVER_STATE, 130 | DELETED_STATE: DELETED_STATE, 131 | 132 | create(type1, id1, relationship1, type2, id2, relationship2, state) { 133 | Ember.assert('Invalid type or ID', type1 && id1 && type2 && id2); 134 | Ember.assert('First relationship must have a name', relationship1); 135 | Ember.assert('Second relationship must have a name or be null', 136 | relationship2 === null || Ember.typeOf(relationship2) === 'string'); 137 | Ember.assert('Invalid state', state === CLIENT_STATE || state === SERVER_STATE || state === DELETED_STATE); 138 | 139 | const id = generateUUID(); 140 | return this._super({ id, type1, id1, relationship1, type2, id2, relationship2, state }); 141 | } 142 | }); 143 | 144 | export default Relationship; -------------------------------------------------------------------------------- /src/store/record_request_cache.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Object.extend({ 4 | 5 | cache: null, 6 | 7 | initializeCache: Ember.on('init', function() { 8 | this.set('cache', Ember.Object.create()); 9 | }), 10 | 11 | _getAndCreateTypeCache(typeKey) { 12 | if (!this.get('cache.' + typeKey)) { 13 | const cache = Ember.Object.create({ 14 | all: null, 15 | single: {}, 16 | multiple: {}, 17 | query: {} 18 | }); 19 | 20 | this.set(`cache.${typeKey}`, cache); 21 | } 22 | 23 | return this.get(`cache.${typeKey}`); 24 | }, 25 | 26 | savePendingRequest(typeKey /* options, request */) { // eslint-disable-line no-inline-comments 27 | const options = (arguments.length > 2 ? arguments[1] : undefined); 28 | const request = (arguments.length > 2 ? arguments[2] : arguments[1]); 29 | 30 | switch (Ember.typeOf(options)) { 31 | case 'string': 32 | case 'number': 33 | this._savePendingSingleRequest(typeKey, options + '', request); 34 | break; 35 | case 'array': 36 | this._savePendingManyRequest(typeKey, options.toArray(), request); 37 | break; 38 | case 'object': 39 | this._savePendingQueryRequest(typeKey, options, request); 40 | break; 41 | case 'undefined': 42 | this._savePendingAllRequest(typeKey, request); 43 | break; 44 | } 45 | }, 46 | 47 | _savePendingSingleRequest(typeKey, id, request) { 48 | const cache = this._getAndCreateTypeCache(typeKey).get('single'); 49 | 50 | cache[id] = request; 51 | 52 | const callback = () => { 53 | cache[id] = null; 54 | }; 55 | 56 | request.then(callback, callback); 57 | }, 58 | 59 | _savePendingManyRequest(typeKey, ids, request) { 60 | const cache = this._getAndCreateTypeCache(typeKey).get('multiple'); 61 | const idString = ids.map((id) => id + '').sort().join(','); 62 | 63 | cache[idString] = request; 64 | 65 | const callback = () => { 66 | cache[idString] = null; 67 | }; 68 | 69 | request.then(callback, callback); 70 | }, 71 | 72 | _savePendingQueryRequest(typeKey, query, request) { 73 | // TODO 74 | }, 75 | 76 | _savePendingAllRequest(typeKey, request) { 77 | const cache = this._getAndCreateTypeCache(typeKey); 78 | 79 | cache.set('all', request); 80 | 81 | const callback = () => { 82 | cache.set('all', null); 83 | }; 84 | 85 | request.then(callback, callback); 86 | }, 87 | 88 | getPendingRequest(typeKey, options) { 89 | switch (Ember.typeOf(options)) { 90 | case 'string': 91 | case 'number': 92 | return this._getPendingSingleRequest(typeKey, options + ''); 93 | case 'array': 94 | return this._getPendingManyRequest(typeKey, options.toArray()); 95 | case 'object': 96 | return this._getPendingQueryRequest(typeKey, options); 97 | case 'undefined': 98 | return this._getPendingAllRequest(typeKey); 99 | default: 100 | return null; 101 | } 102 | }, 103 | 104 | _getPendingSingleRequest(typeKey, id) { 105 | const cache = this._getAndCreateTypeCache(typeKey); 106 | 107 | const all = cache.get('all'); 108 | if (all) { 109 | return all; 110 | } 111 | 112 | const single = cache.get('single')[id]; 113 | if (single) { 114 | return single; 115 | } 116 | 117 | const multiple = cache.get('multiple'); 118 | for (let key in multiple) { 119 | if (multiple.hasOwnProperty(key)) { 120 | if (key.split(',').indexOf(id) >= 0) { 121 | return multiple[key]; 122 | } 123 | } 124 | } 125 | 126 | return null; 127 | }, 128 | 129 | _getPendingManyRequest(typeKey, ids) { 130 | const cache = this._getAndCreateTypeCache(typeKey); 131 | 132 | const all = cache.get('all'); 133 | if (all) { 134 | return all; 135 | } 136 | 137 | const idString = ids.map((id) => id + '').sort().join(','); 138 | 139 | const multiple = cache.get('multiple'); 140 | for (var key in multiple) { 141 | if (multiple.hasOwnProperty(key)) { 142 | if (key === idString) { 143 | return multiple[key]; 144 | } 145 | } 146 | } 147 | 148 | return null; 149 | }, 150 | 151 | _getPendingQueryRequest(typeKey, query) { 152 | // TODO 153 | return null; 154 | }, 155 | 156 | _getPendingAllRequest(typeKey) { 157 | var cache = this._getAndCreateTypeCache(typeKey); 158 | return cache.get('all') || null; 159 | } 160 | 161 | }); -------------------------------------------------------------------------------- /src/relationship/relationship_store.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Relationship from 'ember-graph/relationship/relationship'; 3 | import EmberGraphSet from 'ember-graph/util/set'; 4 | 5 | 6 | var CLIENT_STATE = Relationship.CLIENT_STATE; 7 | var SERVER_STATE = Relationship.SERVER_STATE; 8 | var DELETED_STATE = Relationship.DELETED_STATE; 9 | 10 | var STATE_MAP = {}; 11 | STATE_MAP[CLIENT_STATE] = 'client'; 12 | STATE_MAP[SERVER_STATE] = 'server'; 13 | STATE_MAP[DELETED_STATE] = 'deleted'; 14 | 15 | var RelationshipMap = Ember.Object.extend({ 16 | 17 | length: 0, 18 | 19 | addRelationship: function(name, relationship) { 20 | if (this.hasOwnProperty(name)) { 21 | this.set(name + '.' + relationship.get('id'), relationship); 22 | this.notifyPropertyChange(name); 23 | } else { 24 | var o = Ember.Object.create(); 25 | o.set(relationship.get('id'), relationship); 26 | this.set(name, o); 27 | } 28 | 29 | this.incrementProperty('length'); 30 | }, 31 | 32 | removeRelationship: function(id) { 33 | Object.keys(this).forEach(function(key) { 34 | if (key === 'length') { 35 | return; 36 | } 37 | 38 | var o = this.get(key); 39 | if (typeof o === 'object' && o.hasOwnProperty(id)) { 40 | delete o[id]; 41 | this.notifyPropertyChange(key); 42 | this.decrementProperty('length'); 43 | } 44 | }, this); 45 | }, 46 | 47 | getRelationships: function(name) { 48 | var relationships = this.get(name) || {}; 49 | 50 | return Object.keys(relationships).map(function(key) { 51 | return relationships[key]; 52 | }); 53 | }, 54 | 55 | getAllRelationships: function() { 56 | var relationships = []; 57 | var keys = EmberGraphSet.create(); 58 | keys.addObjects(Object.keys(this)); 59 | keys = keys.without('length'); 60 | 61 | keys.forEach(function(key) { 62 | relationships = relationships.concat(this.getRelationships(key)); 63 | }, this); 64 | 65 | return relationships; 66 | }, 67 | 68 | clearRelationships: function(name) { 69 | this.set(name, Ember.Object.create()); 70 | this.recalculateLength(); 71 | }, 72 | 73 | recalculateLength: function() { 74 | var length = 0; 75 | 76 | Object.keys(this).forEach(function(key) { 77 | if (key !== 'length') { 78 | length += Object.keys(this[key]).length; 79 | } 80 | }, this); 81 | 82 | this.set('length', length); 83 | } 84 | 85 | }); 86 | 87 | export default Ember.Object.extend({ 88 | 89 | server: null, 90 | 91 | client: null, 92 | 93 | deleted: null, 94 | 95 | initializeMaps: Ember.on('init', function() { 96 | this.setProperties({ 97 | server: RelationshipMap.create(), 98 | client: RelationshipMap.create(), 99 | deleted: RelationshipMap.create() 100 | }); 101 | }), 102 | 103 | addRelationship: function(name, relationship) { 104 | if (name === null) { 105 | return; 106 | } 107 | 108 | return this.get(STATE_MAP[relationship.get('state')]).addRelationship(name, relationship); 109 | }, 110 | 111 | removeRelationship: function(id) { 112 | if (Ember.typeOf(id) !== 'string') { 113 | id = Ember.get(id, 'id'); // eslint-disable-line no-param-reassign 114 | } 115 | 116 | this.get('server').removeRelationship(id); 117 | this.get('client').removeRelationship(id); 118 | this.get('deleted').removeRelationship(id); 119 | }, 120 | 121 | clearRelationships: function(name) { 122 | this.get('server').clearRelationships(name); 123 | this.get('client').clearRelationships(name); 124 | this.get('deleted').clearRelationships(name); 125 | }, 126 | 127 | getServerRelationships: function(name) { 128 | return this.get('server').getRelationships(name).concat(this.get('deleted').getRelationships(name)); 129 | }, 130 | 131 | getCurrentRelationships: function(name) { 132 | return this.get('server').getRelationships(name).concat(this.get('client').getRelationships(name)); 133 | }, 134 | 135 | getRelationshipsByState: function(state) { 136 | return this.get(STATE_MAP[state]).getAllRelationships(); 137 | }, 138 | 139 | getRelationshipsByName: function(name) { 140 | var server = this.get('server').getRelationships(name); 141 | var client = this.get('client').getRelationships(name); 142 | var deleted = this.get('deleted').getRelationships(name); 143 | 144 | return server.concat(client).concat(deleted); 145 | } 146 | }); -------------------------------------------------------------------------------- /src/adapter/ember_graph/server.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { pluralize } from 'ember-graph/util/inflector'; 4 | import { generateUUID } from 'ember-graph/util/util'; 5 | 6 | 7 | export default { 8 | 9 | /** 10 | * @method serverCreateRecord 11 | * @param {String} typeKey 12 | * @param {JSON} json 13 | * @return {Promise} 14 | * @protected 15 | * @for EmberGraphAdapter 16 | */ 17 | serverCreateRecord: function(typeKey, json) { 18 | let newId = null; 19 | 20 | return this.getDatabase().then((db) => { 21 | newId = this.generateIdForRecord(typeKey, json, db); 22 | 23 | const modifiedDb = this.putRecordInDatabase(typeKey, newId, json[pluralize(typeKey)][0], db); 24 | return this.setDatabase(modifiedDb); 25 | }).then((db) => { 26 | const record = this.getRecordFromDatabase(typeKey, newId, db); 27 | return { 28 | [pluralize(typeKey)]: [record] 29 | }; 30 | }); 31 | }, 32 | 33 | /** 34 | * @method serverFindRecord 35 | * @param {String} typeKey 36 | * @param {String} id 37 | * @return {Promise} 38 | * @protected 39 | * @for EmberGraphAdapter 40 | */ 41 | serverFindRecord: function(typeKey, id) { 42 | var _this = this; 43 | 44 | return this.getDatabase().then(function(db) { 45 | if (Ember.get(db, 'records.' + typeKey + '.' + id)) { 46 | var payload = {}; 47 | payload[pluralize(typeKey)] = [_this.getRecordFromDatabase(typeKey, id, db)]; 48 | return payload; 49 | } else { 50 | throw { status: 404, typeKey: typeKey, id: id }; 51 | } 52 | }); 53 | }, 54 | 55 | /** 56 | * @method serverFindMany 57 | * @param {String} typeKey 58 | * @param {String[]} ids 59 | * @return {Promise} 60 | * @protected 61 | * @for EmberGraphAdapter 62 | */ 63 | serverFindMany: function(typeKey, ids) { 64 | var _this = this; 65 | 66 | return this.getDatabase().then(function(db) { 67 | var records = ids.map(function(id) { 68 | if (Ember.get(db, 'records.' + typeKey + '.' + id)) { 69 | return _this.getRecordFromDatabase(typeKey, id, db); 70 | } else { 71 | throw { status: 404, typeKey: typeKey, id: id }; 72 | } 73 | }); 74 | 75 | var payload = {}; 76 | payload[pluralize(typeKey)] = records; 77 | return payload; 78 | }); 79 | }, 80 | 81 | /** 82 | * @method serverFindAll 83 | * @param {String} typeKey 84 | * @return {Promise} 85 | * @protected 86 | * @for EmberGraphAdapter 87 | */ 88 | serverFindAll: function(typeKey) { 89 | var _this = this; 90 | 91 | return this.getDatabase().then(function(db) { 92 | var records = Object.keys(db.records[typeKey] || {}).map(function(id) { 93 | return _this.getRecordFromDatabase(typeKey, id, db); 94 | }); 95 | 96 | var payload = {}; 97 | payload[pluralize(typeKey)] = records; 98 | return payload; 99 | }); 100 | }, 101 | 102 | /** 103 | * @method serverUpdateRecord 104 | * @param {String} typeKey 105 | * @param {String} id 106 | * @param {JSON[]} changes 107 | * @return {Promise} Resolves to update record payload 108 | * @protected 109 | * @for EmberGraphAdapter 110 | */ 111 | serverUpdateRecord: function(typeKey, id, changes) { 112 | return this.getDatabase().then((db) => { 113 | const modifiedDb = this.applyChangesToDatabase(typeKey, id, changes, db); 114 | return this.setDatabase(modifiedDb); 115 | }).then((db) => { 116 | return { 117 | [pluralize(typeKey)]: [this.getRecordFromDatabase(typeKey, id, db)] 118 | }; 119 | }); 120 | }, 121 | 122 | /** 123 | * @method serverDeleteRecord 124 | * @param {String} typeKey 125 | * @param {String} id 126 | * @return {Promise} 127 | * @protected 128 | * @for EmberGraphAdapter 129 | */ 130 | serverDeleteRecord: function(typeKey, id) { 131 | return this.getDatabase().then((db) => { 132 | if (db.records[typeKey]) { 133 | delete db.records[typeKey][id]; 134 | } 135 | 136 | db.relationships = db.relationships.filter((r) => { 137 | return !((r.t1 === typeKey && r.i1 === id) || (r.t2 === typeKey && r.i2 === id)); 138 | }); 139 | 140 | return this.setDatabase(db).then(() => { 141 | return { 142 | meta: { 143 | deletedRecords: [{ type: typeKey, id: id }] 144 | } 145 | }; 146 | }); 147 | }); 148 | }, 149 | 150 | /** 151 | * @method generateIdForRecord 152 | * @param {String} typeKey 153 | * @param {JSON} json 154 | * @param {JSON} db 155 | * @return {String} 156 | * @protected 157 | * @for EmberGraphAdapter 158 | */ 159 | generateIdForRecord: function(typeKey, json, db) { 160 | return generateUUID(); 161 | } 162 | 163 | }; -------------------------------------------------------------------------------- /src/adapter/adapter.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { abstractMethod } from 'ember-graph/util/util'; 4 | 5 | /** 6 | * An interface for an adapter. And adapter is used to communicate with 7 | * the server. The adapter is never called directly, its methods are 8 | * called by the store to perform its operations. 9 | * 10 | * The adapter should return normalized JSON from its operations. Details 11 | * about normalized JSON can be found in the {{link-to-method 'Store' 'pushPayload'}} 12 | * documentation. 13 | * 14 | * @class Adapter 15 | * @constructor 16 | * @category abstract 17 | */ 18 | export default Ember.Object.extend({ 19 | 20 | /** 21 | * The store that this adapter belongs to. 22 | * This might be needed to get models and their metadata. 23 | * 24 | * @property store 25 | * @type Store 26 | * @final 27 | */ 28 | store: null, 29 | 30 | /** 31 | * Persists a record to the server. The returned JSON 32 | * must include the `newId` meta attribute as described 33 | * {{link-to-method 'here' 'Serializer' 'deserialize'}}. 34 | * 35 | * @method createRecord 36 | * @param {Model} record 37 | * @return {Promise} Resolves to the normalized JSON 38 | * @category abstract 39 | */ 40 | createRecord: abstractMethod('createRecord'), 41 | 42 | /** 43 | * Fetch a record from the server. 44 | * 45 | * @method findRecord 46 | * @param {String} typeKey 47 | * @param {String} id 48 | * @return {Promise} Resolves to the normalized JSON 49 | * @category abstract 50 | */ 51 | findRecord: abstractMethod('findRecord'), 52 | 53 | /** 54 | * The same as find, only it should load several records. 55 | * 56 | * @method findMany 57 | * @param {String} typeKey 58 | * @param {String[]} ids 59 | * @return {Promise} Resolves to the normalized JSON 60 | * @category abstract 61 | */ 62 | findMany: abstractMethod('findMany'), 63 | 64 | /** 65 | * The same as find, only it should load all records of the given type. 66 | * 67 | * @method findAll 68 | * @param {String} typeKey 69 | * @return {Promise} Resolves to the normalized JSON 70 | * @category abstract 71 | */ 72 | findAll: abstractMethod('findAll'), 73 | 74 | /** 75 | * Queries the server for records of the given type. The resolved 76 | * JSON should include the `queryIds` meta attribute as 77 | * described {{link-to-method 'here' 'Serializer' 'deserialize'}}. 78 | * 79 | * @method findQuery 80 | * @param {String} typeKey 81 | * @param {Object} query The query object passed into the store's `find` method 82 | * @return {Promise} Resolves to the normalized JSON 83 | * @category abstract 84 | */ 85 | findQuery: abstractMethod('findQuery'), 86 | 87 | /** 88 | * Saves the record's changes to the server. 89 | * 90 | * @method updateRecord 91 | * @param {Model} record 92 | * @return {Promise} Resolves to the normalized JSON 93 | * @category abstract 94 | */ 95 | updateRecord: abstractMethod('updateRecord'), 96 | 97 | /** 98 | * Deletes the record. 99 | * 100 | * @method deleteRecord 101 | * @param {Model} record 102 | * @return {Promise} Resolves to the normalized JSON 103 | * @category abstract 104 | */ 105 | deleteRecord: abstractMethod('deleteRecord'), 106 | 107 | /** 108 | * Gets the serializer specified for a type. It first tries to get 109 | * a type-specific serializer. If it can't find one, it tries to use 110 | * the application serializer. If it can't find one, it uses the default 111 | * {{link-to-class 'JSONSerializer'}}. 112 | * 113 | * @method serializerFor 114 | * @param {String} typeKey 115 | * @return {Serializer} 116 | * @protected 117 | */ 118 | serializerFor: function(typeKey) { 119 | return this.get('store').serializerFor(typeKey); 120 | }, 121 | 122 | /** 123 | * Serializes the given record. By default, it defers to the serializer. 124 | * 125 | * @method serialize 126 | * @param {Model} record 127 | * @param {Object} options 128 | * @return {Object} Serialized record 129 | * @protected 130 | */ 131 | serialize: function(record, options) { 132 | return this.serializerFor(record.get('typeKey')).serialize(record, options); 133 | }, 134 | 135 | /** 136 | * Deserializes the given payload. By default, it defers to the serializer. 137 | * 138 | * @method deserialize 139 | * @param {JSON} payload 140 | * @param {Object} options 141 | * @return {Object} Normalized JSON payload 142 | * @protected 143 | */ 144 | deserialize: function(payload, options) { 145 | return this.serializerFor(options.recordType).deserialize(payload, options); 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /test/store/record_retrieval_cache.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Promise = Em.RSVP.Promise; 5 | 6 | var typeKey = 'test'; 7 | var cache; 8 | 9 | module('Record Retrieval Cache Test', { 10 | setup: function() { 11 | cache = EG.RecordRequestCache.create(); 12 | } 13 | }); 14 | 15 | test('Returns null for empty cache', function() { 16 | expect(5); 17 | 18 | strictEqual(cache.getPendingRequest(typeKey), null); 19 | strictEqual(cache.getPendingRequest(typeKey, '10'), null); 20 | strictEqual(cache.getPendingRequest(typeKey, 10), null); 21 | strictEqual(cache.getPendingRequest(typeKey, ['1', '2', 3]), null); 22 | strictEqual(cache.getPendingRequest(typeKey, { foo: 'bar' }), null); 23 | }); 24 | 25 | test('Single ID retrieval', function() { 26 | expect(1); 27 | 28 | var promise = new Promise(function() {}); 29 | cache.savePendingRequest(typeKey, 10, promise); 30 | strictEqual(cache.getPendingRequest(typeKey, '10'), promise); 31 | }); 32 | 33 | test('Multiple ID retrieval', function() { 34 | expect(1); 35 | 36 | var promise = new Promise(function() {}); 37 | cache.savePendingRequest(typeKey, ['1', '2', 3], promise); 38 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, '3']), promise); 39 | }); 40 | 41 | test('All type retrieval', function() { 42 | expect(1); 43 | 44 | var promise = new Promise(function() {}); 45 | cache.savePendingRequest(typeKey, promise); 46 | strictEqual(cache.getPendingRequest(typeKey), promise); 47 | }); 48 | 49 | test('Query retrieval is unimplemented', function() { 50 | expect(1); 51 | 52 | var promise = new Promise(function() {}); 53 | cache.savePendingRequest(typeKey, {}, promise); 54 | strictEqual(cache.getPendingRequest(typeKey), null); 55 | }); 56 | 57 | test('Single ID chains all type request', function() { 58 | expect(2); 59 | 60 | var promise = new Promise(function() {}); 61 | cache.savePendingRequest(typeKey, promise); 62 | strictEqual(cache.getPendingRequest(typeKey, '1'), promise); 63 | strictEqual(cache.getPendingRequest(typeKey, 100), promise); 64 | }); 65 | 66 | test('Multiple IDs chains all type request', function() { 67 | expect(1); 68 | 69 | var promise = new Promise(function() {}); 70 | cache.savePendingRequest(typeKey, promise); 71 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise); 72 | }); 73 | 74 | test('Single ID chains multiple ID request', function() { 75 | expect(2); 76 | 77 | var promise = new Promise(function() {}); 78 | cache.savePendingRequest(typeKey, [1, 2, 3], promise); 79 | strictEqual(cache.getPendingRequest(typeKey, '1'), promise); 80 | strictEqual(cache.getPendingRequest(typeKey, 3), promise); 81 | }); 82 | 83 | test('Multiple ID chains request with same IDs', function() { 84 | expect(2); 85 | 86 | var promise = new Promise(function() {}); 87 | cache.savePendingRequest(typeKey, [1, 2, 3], promise); 88 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise); 89 | strictEqual(cache.getPendingRequest(typeKey, [3, 1, 2]), promise); 90 | }); 91 | 92 | asyncTest('Single ID promise gets removed', function() { 93 | expect(2); 94 | 95 | var promise = Promise.resolve(); 96 | cache.savePendingRequest(typeKey, '1', promise); 97 | 98 | start(); 99 | strictEqual(cache.getPendingRequest(typeKey, '1'), promise); 100 | stop(); 101 | 102 | Em.run.next(function() { 103 | start(); 104 | strictEqual(cache.getPendingRequest(typeKey, '1'), null); 105 | }); 106 | }); 107 | 108 | asyncTest('Multiple IDs promise gets removed', function() { 109 | expect(4); 110 | 111 | var promise = Promise.resolve(); 112 | cache.savePendingRequest(typeKey, [1, 2, 3], promise); 113 | 114 | start(); 115 | strictEqual(cache.getPendingRequest(typeKey, '1'), promise); 116 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise); 117 | stop(); 118 | 119 | Em.run.next(function() { 120 | start(); 121 | strictEqual(cache.getPendingRequest(typeKey, '1'), null); 122 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), null); 123 | }); 124 | }); 125 | 126 | asyncTest('All type promise gets removed', function() { 127 | expect(6); 128 | 129 | var promise = Promise.resolve(); 130 | cache.savePendingRequest(typeKey, promise); 131 | 132 | start(); 133 | strictEqual(cache.getPendingRequest(typeKey), promise); 134 | strictEqual(cache.getPendingRequest(typeKey, '1'), promise); 135 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), promise); 136 | stop(); 137 | 138 | Em.run.next(function() { 139 | start(); 140 | strictEqual(cache.getPendingRequest(typeKey), null); 141 | strictEqual(cache.getPendingRequest(typeKey, '1'), null); 142 | strictEqual(cache.getPendingRequest(typeKey, [1, 2, 3]), null); 143 | }); 144 | }); 145 | })(); -------------------------------------------------------------------------------- /src/store/lookup.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { deprecateMethod } from 'ember-graph/util/util'; 4 | 5 | export default { 6 | 7 | /** 8 | * Stores the models used so far. This not ony caches them so we don't 9 | * have to hit the container, but it also let's use know that the 10 | * typeKey has been property injected into them. 11 | * 12 | * @property modelCache 13 | * @type {Object} 14 | * @final 15 | * @private 16 | */ 17 | modelCache: {}, 18 | 19 | /** 20 | * Stores attribute types as they're looked up in the container. 21 | * @property attributeTypeCache 22 | * @type {Object} 23 | * @final 24 | * @private 25 | */ 26 | attributeTypeCache: {}, 27 | 28 | /** 29 | * Stores adapters as they're looked up in the container. 30 | * 31 | * @property adapterCache 32 | * @type Object 33 | * @final 34 | * @private 35 | */ 36 | adapterCache: {}, 37 | 38 | /** 39 | * Stores serializers as they're looked up in the container. 40 | * 41 | * @property adapterCache 42 | * @type Object 43 | * @final 44 | * @private 45 | */ 46 | serializerCache: {}, 47 | 48 | initializeLookupCaches: Ember.on('init', function() { 49 | this.setProperties({ 50 | modelCache: {}, 51 | attributeTypeCache: {}, 52 | adapterCache: {}, 53 | serializerCache: {} 54 | }); 55 | }), 56 | 57 | modelForType: deprecateMethod('`modelForType` deprecated in favor of `modelFor`', 'modelFor'), 58 | 59 | /** 60 | * Looks up the model for the specified typeKey. The `typeKey` property 61 | * isn't available on the class or its instances until the type is 62 | * looked up with this method for the first time. 63 | * 64 | * @method modelFor 65 | * @param {String} typeKey 66 | * @return {Class} 67 | */ 68 | modelFor(typeKey) { 69 | const modelCache = this.get('modelCache'); 70 | 71 | if (!modelCache[typeKey]) { 72 | const model = Ember.getOwner(this).factoryFor(`model:${typeKey}`); 73 | if (!model) { 74 | throw new Ember.Error(`Cannot find model class with typeKey: ${typeKey}`); 75 | } 76 | 77 | model.class.reopen({ typeKey }); 78 | model.class.reopenClass({ typeKey }); 79 | modelCache[typeKey] = model.class; 80 | } 81 | 82 | return modelCache[typeKey]; 83 | }, 84 | 85 | /** 86 | * Returns an `AttributeType` instance for the given named type. 87 | * 88 | * @method attributeTypeFor 89 | * @param {String} typeName 90 | * @return {AttributeType} 91 | */ 92 | attributeTypeFor(typeName) { 93 | const attributeTypeCache = this.get('attributeTypeCache'); 94 | 95 | if (!attributeTypeCache[typeName]) { 96 | attributeTypeCache[typeName] = Ember.getOwner(this).lookup(`type:${typeName}`); 97 | 98 | if (!attributeTypeCache[typeName]) { 99 | throw new Ember.Error(`Cannot find attribute type with name: ${typeName}`); 100 | } 101 | } 102 | 103 | return attributeTypeCache[typeName]; 104 | }, 105 | 106 | /** 107 | * Gets the adapter for the specified type. First, it looks for a type-specific 108 | * adapter. If one isn't found, it looks for the application adapter. If that 109 | * isn't found, it uses the default {{link-to-class 'RESTAdapter'}}. 110 | * 111 | * Note that this method will cache the results, so your adapter configuration 112 | * must be finalized before the app starts up. 113 | * 114 | * @method adapterFor 115 | * @param {String} typeKey 116 | * @return {Adapter} 117 | * @protected 118 | */ 119 | adapterFor(typeKey) { 120 | const adapterCache = this.get('adapterCache'); 121 | 122 | if (!adapterCache[typeKey]) { 123 | const container = Ember.getOwner(this); 124 | 125 | adapterCache[typeKey] = container.lookup(`adapter:${typeKey}`) || 126 | container.lookup('adapter:application') || 127 | container.lookup('adapter:rest'); 128 | } 129 | 130 | return adapterCache[typeKey]; 131 | }, 132 | 133 | /** 134 | * Gets the serializer for the specified type. First, it looks for a type-specific 135 | * serializer. If one isn't found, it looks for the application serializer. If that 136 | * isn't found, it uses the default {{link-to-class 'JSONSerializer'}}. 137 | * 138 | * Note that this method will cache the results, so your serializer configuration 139 | * must be finalized before the app starts up. 140 | * 141 | * @method serializerFor 142 | * @param {String} typeKey 143 | * @return {Serializer} 144 | * @protected 145 | */ 146 | serializerFor(typeKey) { 147 | const serializerCache = this.get('serializerCache'); 148 | 149 | if (!serializerCache[typeKey]) { 150 | const container = Ember.getOwner(this); 151 | 152 | serializerCache[typeKey] = 153 | container.lookup('serializer:' + (typeKey || 'application')) || 154 | container.lookup('serializer:application') || 155 | container.lookup('serializer:json'); 156 | } 157 | 158 | return serializerCache[typeKey]; 159 | } 160 | 161 | }; 162 | -------------------------------------------------------------------------------- /src/util/util.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import EmberGraphSet from 'ember-graph/util/set'; 3 | 4 | import { computed } from 'ember-graph/util/computed'; 5 | 6 | /** 7 | * Denotes that method must be implemented in a subclass. 8 | * If it's not overridden, calling it will throw an error. 9 | * 10 | * ```js 11 | * var Shape = Ember.Object.extend({ 12 | * getNumberOfSides: EG.abstractMethod('getNumberOfSides') 13 | * }); 14 | * ``` 15 | * 16 | * @method abstractMethod 17 | * @param {String} methodName 18 | * @return {Function} 19 | * @namespace EmberGraph 20 | */ 21 | function abstractMethod(methodName) { 22 | return function() { 23 | throw new Ember.Error('You failed to implement the abstract `' + methodName + '` method.'); 24 | }; 25 | } 26 | 27 | /** 28 | * Denotes that a property must be overridden in a subclass. 29 | * If it's not overridden, using it will throw an error. 30 | * 31 | * ```js 32 | * var Shape = Ember.Object.extend({ 33 | * name: EG.abstractProperty('name') 34 | * }); 35 | * ``` 36 | * 37 | * @method abstractProperty 38 | * @param {String} propertyName 39 | * @return {ComputedProperty} 40 | * @namespace EmberGraph 41 | */ 42 | function abstractProperty(propertyName) { 43 | return computed({ 44 | get() { 45 | throw new Ember.Error('You failed to override the abstract `' + propertyName + '` property.'); 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Generates a version 4 (random) UUID. 52 | * 53 | * @method generateUUID 54 | * @return {String} 55 | * @namespace EmberGraph 56 | */ 57 | function generateUUID() { 58 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 59 | var r = Math.random() * 16|0; // eslint-disable-line 60 | var v = (c == 'x' ? r : (r&0x3|0x8)); // eslint-disable-line 61 | return v.toString(16); 62 | }); 63 | } 64 | 65 | /** 66 | * Compares the contents of two arrays for equality. Uses 67 | * Ember.Set to make the comparison, so the objects must 68 | * be equal with `===`. 69 | * 70 | * @method arrayContentsEqual 71 | * @param {Array} a 72 | * @param {Array} b 73 | * @returns {Boolean} 74 | * @namespace EmberGraph 75 | */ 76 | function arrayContentsEqual(a, b) { 77 | var set = EmberGraphSet.create(); 78 | set.addObjects(a); 79 | return (a.length === b.length && set.isEqual(b)); 80 | } 81 | 82 | /** 83 | * Takes a list of record objects (with `type` and `id`) 84 | * and groups them into arrays based on their type. 85 | * 86 | * @method groupRecords 87 | * @param {Object[]} records 88 | * @return {Array[]} 89 | * @namespace EmberGraph 90 | */ 91 | function groupRecords(records) { 92 | var groups = records.reduce(function(groups, record) { 93 | if (groups[record.type]) { 94 | groups[record.type].push(record); 95 | } else { 96 | groups[record.type] = [record]; 97 | } 98 | 99 | return groups; 100 | }, {}); 101 | 102 | return Object.keys(groups).reduce(function(array, key) { 103 | if (groups[key].length > 0) { 104 | array.push(groups[key]); 105 | } 106 | 107 | return array; 108 | }, []); 109 | } 110 | 111 | /** 112 | * Calls `callback` once for each value of the given object. 113 | * The callback receives `key` and `value` parameters. 114 | * 115 | * @method values 116 | * @param {Object} obj 117 | * @param {Function} callback 118 | * @param {Any} [thisArg=undefined] 119 | * @namespace EmberGraph 120 | */ 121 | function values(obj, callback, thisArg) { 122 | var keys = Object.keys(obj); 123 | 124 | for (var i = 0; i < keys.length; ++i) { 125 | callback.call(thisArg, keys[i], obj[keys[i]]); 126 | } 127 | } 128 | 129 | /** 130 | * Works like `Ember.aliasMethod` only it displays a 131 | * deprecation warning before the aliased method is called. 132 | * 133 | * @method deprecateMethod 134 | * @param {String} message 135 | * @param {String} method 136 | * @return {Function} 137 | * @namespace EmberGraph 138 | */ 139 | function deprecateMethod(message, method) { 140 | return function() { 141 | Ember.deprecate(message, false, { id: method, until: '2.0.0' }); 142 | this[method].apply(this, arguments); 143 | }; 144 | } 145 | 146 | /** 147 | * Works like 'Ember.computed.alias' only it displays a 148 | * deprecation warning before the aliased property is returned. 149 | * 150 | * @method deprecateProperty 151 | * @param {String} message 152 | * @param {String} property 153 | * @return {ComputedProperty} 154 | * @namespace EmberGraph 155 | */ 156 | function deprecateProperty(message, property) { 157 | return computed(property, { 158 | get() { 159 | Ember.deprecate(message, false, { id: property, until: '2.0.0' }); 160 | return this.get(property); 161 | }, 162 | set(key, value) { 163 | this.set(property, value); 164 | } 165 | }); 166 | } 167 | 168 | export { 169 | abstractMethod, 170 | abstractProperty, 171 | generateUUID, 172 | arrayContentsEqual, 173 | groupRecords, 174 | values, 175 | deprecateMethod, 176 | deprecateProperty 177 | }; 178 | -------------------------------------------------------------------------------- /src/util/set.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Copyable from 'ember-graph/util/copyable'; 3 | 4 | /* eslint-disable */ 5 | /** 6 | * Pulled from the Ember 1.13 release. 7 | * 8 | * withoutAll added by me 9 | * 10 | * TODO: Remove and use ES6 Set 11 | */ 12 | export default Ember.CoreObject.extend(Ember.MutableArray, Copyable, { 13 | 14 | length: 0, 15 | 16 | clear() { 17 | var len = Ember.get(this, 'length'); 18 | if (len === 0) { return this; } 19 | 20 | var guid; 21 | 22 | this.arrayContentWillChange(len, 0); 23 | 24 | for (var i=0; i < len; i++) { 25 | guid = Ember.guidFor(this[i]); 26 | delete this[guid]; 27 | delete this[i]; 28 | } 29 | 30 | Ember.set(this, 'length', 0); 31 | 32 | Ember.notifyPropertyChange(this, 'firstObject'); 33 | Ember.notifyPropertyChange(this, 'lastObject'); 34 | this.arrayContentDidChange(len, 0); 35 | 36 | return this; 37 | }, 38 | 39 | isEqual(obj) { 40 | // fail fast 41 | if (!Ember.Enumerable.detect(obj)) { 42 | return false; 43 | } 44 | 45 | var loc = Ember.get(this, 'length'); 46 | if (Ember.get(obj, 'length') !== loc) { 47 | return false; 48 | } 49 | 50 | while (--loc >= 0) { 51 | if (!obj.includes(this[loc])) { 52 | return false; 53 | } 54 | } 55 | 56 | return true; 57 | }, 58 | 59 | add() { 60 | return this.addObject(...arguments); 61 | }, 62 | 63 | remove() { 64 | return this.removeObject(...arguments); 65 | }, 66 | 67 | pop() { 68 | var obj = this.length > 0 ? this[this.length-1] : null; 69 | this.remove(obj); 70 | return obj; 71 | }, 72 | 73 | push() { 74 | return this.addObject(...arguments); 75 | }, 76 | 77 | shift() { 78 | return this.pop(...arguments); 79 | }, 80 | 81 | unshift() { 82 | return this.push(...arguments); 83 | }, 84 | 85 | addEach() { 86 | return this.addObject(...arguments); 87 | }, 88 | 89 | removeEach() { 90 | return this.removeObjects(...arguments); 91 | }, 92 | 93 | init(items) { 94 | this._super(...arguments); 95 | 96 | if (items) { 97 | this.addObjects(items); 98 | } 99 | }, 100 | 101 | nextObject(idx) { 102 | return this[idx]; 103 | }, 104 | 105 | firstObject: Ember.computed(function() { 106 | return this.length > 0 ? this[0] : undefined; 107 | }), 108 | 109 | lastObject: Ember.computed(function() { 110 | return this.length > 0 ? this[this.length-1] : undefined; 111 | }), 112 | 113 | addObject(obj) { 114 | if (Ember.isNone(obj)) { 115 | return this; // nothing to do 116 | } 117 | 118 | var guid = Ember.guidFor(obj); 119 | var idx = this[guid]; 120 | var len = Ember.get(this, 'length'); 121 | var added; 122 | 123 | if (idx>=0 && idx=0 && idx=0; 181 | }, 182 | 183 | copy() { 184 | var C = this.constructor; 185 | var ret = new C(); 186 | var loc = Ember.get(this, 'length'); 187 | 188 | set(ret, 'length', loc); 189 | while (--loc >= 0) { 190 | ret[loc] = this[loc]; 191 | ret[Ember.guidFor(this[loc])] = loc; 192 | } 193 | return ret; 194 | }, 195 | 196 | toString() { 197 | var len = this.length; 198 | var array = []; 199 | var idx; 200 | 201 | for (idx = 0; idx < len; idx++) { 202 | array[idx] = this[idx]; 203 | } 204 | return 'Ember.Set<' + array.join(',') + '>'; 205 | }, 206 | 207 | withoutAll(items) { 208 | var ret = this.copy(); 209 | ret.removeObjects(items); 210 | return ret; 211 | } 212 | }); 213 | /* eslint-enable */ 214 | -------------------------------------------------------------------------------- /src/shim.js: -------------------------------------------------------------------------------- 1 | import EmberGraph from 'ember-graph'; 2 | 3 | // Data Adapter 4 | import DataAdapter from 'ember-graph/util/data_adapter'; 5 | EmberGraph.DataAdapter = DataAdapter; 6 | 7 | // Array polyfills 8 | import { 9 | some, 10 | reduce, 11 | mapBy 12 | } from 'ember-graph/util/array'; 13 | EmberGraph.ArrayPolyfills = { 14 | some: some, 15 | reduce: reduce, 16 | mapBy: mapBy 17 | }; 18 | 19 | // EmberGraph namespace methods 20 | import { 21 | abstractMethod, 22 | abstractProperty, 23 | generateUUID, 24 | arrayContentsEqual, 25 | groupRecords, 26 | values, 27 | deprecateMethod, 28 | deprecateProperty 29 | } from 'ember-graph/util/util'; 30 | EmberGraph.abstractMethod = abstractMethod; 31 | EmberGraph.abstractProperty = abstractProperty; 32 | EmberGraph.generateUUID = generateUUID; 33 | EmberGraph.arrayContentsEqual = arrayContentsEqual; 34 | EmberGraph.groupRecords = groupRecords; 35 | EmberGraph.values = values; 36 | EmberGraph.deprecateMethod = deprecateMethod; 37 | EmberGraph.deprecateProperty = deprecateProperty; 38 | 39 | import { 40 | attr, 41 | hasOne, 42 | hasMany 43 | } from 'ember-graph/model/schema'; 44 | EmberGraph.attr = attr; 45 | EmberGraph.hasOne = hasOne; 46 | EmberGraph.hasMany = hasMany; 47 | 48 | // Set 49 | import EmberGraphSet from 'ember-graph/util/set'; 50 | EmberGraph.Set = EmberGraphSet; 51 | 52 | // String polyfills 53 | import { 54 | startsWith, 55 | endsWith, 56 | capitalize, 57 | decapitalize 58 | } from 'ember-graph/util/string'; 59 | import { 60 | pluralize, 61 | singularize 62 | } from 'ember-graph/util/inflector'; 63 | EmberGraph.String = { 64 | startsWith: startsWith, 65 | endsWith: endsWith, 66 | capitalize: capitalize, 67 | decapitalize: decapitalize, 68 | pluralize: pluralize, 69 | singularize: singularize 70 | }; 71 | 72 | // Promise proxy objects 73 | import { 74 | PromiseObject, 75 | PromiseArray, 76 | ModelPromiseObject 77 | } from 'ember-graph/data/promise_object'; 78 | EmberGraph.PromiseObject = PromiseObject; 79 | EmberGraph.PromiseArray = PromiseArray; 80 | EmberGraph.ModelPromiseObject = ModelPromiseObject; 81 | 82 | // Serializers 83 | import Serializer from 'ember-graph/serializer/serializer'; 84 | EmberGraph.Serializer = Serializer; 85 | import JSONSerializer from 'ember-graph/serializer/json'; 86 | EmberGraph.JSONSerializer = JSONSerializer; 87 | import EmberGraphSerializer from 'ember-graph/serializer/ember_graph'; 88 | EmberGraph.EmberGraphSerializer = EmberGraphSerializer; 89 | 90 | // Adapters 91 | import Adapter from 'ember-graph/adapter/adapter'; 92 | EmberGraph.Adapter = Adapter; 93 | import EmberGraphAdapter from 'ember-graph/adapter/ember_graph/adapter'; 94 | EmberGraph.EmberGraphAdapter = EmberGraphAdapter; 95 | import LocalStorageAdapter from 'ember-graph/adapter/local_storage'; 96 | EmberGraph.LocalStorageAdapter = LocalStorageAdapter; 97 | import MemoryAdapter from 'ember-graph/adapter/memory'; 98 | EmberGraph.MemoryAdapter = MemoryAdapter; 99 | import RESTAdapter from 'ember-graph/adapter/rest'; 100 | EmberGraph.RESTAdapter = RESTAdapter; 101 | 102 | // Store 103 | import Store from 'ember-graph/store/store'; 104 | EmberGraph.Store = Store; 105 | 106 | // Attribute Types 107 | import AttributeType from 'ember-graph/attribute_type/type'; 108 | EmberGraph.AttributeType = AttributeType; 109 | import ArrayType from 'ember-graph/attribute_type/array'; 110 | EmberGraph.ArrayType = ArrayType; 111 | import BooleanType from 'ember-graph/attribute_type/boolean'; 112 | EmberGraph.BooleanType = BooleanType; 113 | import DateType from 'ember-graph/attribute_type/date'; 114 | EmberGraph.DateType = DateType; 115 | import EnumType from 'ember-graph/attribute_type/enum'; 116 | EmberGraph.EnumType = EnumType; 117 | import NumberType from 'ember-graph/attribute_type/number'; 118 | EmberGraph.NumberType = NumberType; 119 | import ObjectType from 'ember-graph/attribute_type/object'; 120 | EmberGraph.ObjectType = ObjectType; 121 | import StringType from 'ember-graph/attribute_type/string'; 122 | EmberGraph.StringType = StringType; 123 | 124 | // Model 125 | import Model from 'ember-graph/model/model'; 126 | EmberGraph.Model = Model; 127 | 128 | // Testing shims 129 | import RelationshipHash from 'ember-graph/relationship/relationship_hash'; 130 | EmberGraph.RelationshipHash = RelationshipHash; 131 | import Relationship from 'ember-graph/relationship/relationship'; 132 | EmberGraph.Relationship = Relationship; 133 | import RelationshipStore from 'ember-graph/relationship/relationship_store'; 134 | EmberGraph.RelationshipStore = RelationshipStore; 135 | import RecordCache from 'ember-graph/store/record_cache'; 136 | EmberGraph.RecordCache = RecordCache; 137 | import RecordRequestCache from 'ember-graph/store/record_request_cache'; 138 | EmberGraph.RecordRequestCache = RecordRequestCache; 139 | 140 | // Inflector 141 | import { 142 | overridePluralRule, 143 | overrideSingularRule 144 | } from 'ember-graph/util/inflector'; 145 | EmberGraph.Inflector = { singularize, pluralize, overridePluralRule, overrideSingularRule }; -------------------------------------------------------------------------------- /src/adapter/ember_graph/adapter.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Adapter from 'ember-graph/adapter/adapter'; 3 | import LoadMethods from 'ember-graph/adapter/ember_graph/load'; 4 | import ServerMethods from 'ember-graph/adapter/ember_graph/server'; 5 | import DatabaseMethods from 'ember-graph/adapter/ember_graph/database'; 6 | 7 | import { computed } from 'ember-graph/util/computed'; 8 | 9 | var Promise = Ember.RSVP.Promise; 10 | 11 | /** 12 | * This class acts as a base adapter for synchronous storage forms. Specifically, 13 | * the {{link-to-class 'LocalStorageAdapter'}} and {{link-to-class 'MemoryAdapter'}} 14 | * inherit from this class. This class will perform all of the work of updating data 15 | * and maintaining data integrity, subclasses need only implement the 16 | * {{link-to-method 'EmberGraphAdapter' 'getDatabase'}} and 17 | * {{link-to-method 'EmberGraphAdapter' 'setDatabase'}} methods to create a 18 | * fully-functioning adapter. This class works with data as a single JSON object 19 | * that takes the following form: 20 | * 21 | * ```json 22 | * { 23 | * "records": { 24 | * "type_key": { 25 | * "record_id": { 26 | * "attr1": "value", 27 | * "attr2": 5 28 | * } 29 | * } 30 | * }, 31 | * "relationships": [{ 32 | * "t1": "type_key", 33 | * "i1": "id", 34 | * "n1": "relationship_name", 35 | * "t2": "type_key", 36 | * "i2": "id", 37 | * "n2": "relationship_name", 38 | * }] 39 | * } 40 | * ``` 41 | * 42 | * If you can store the JSON data, then this adapter will ensure complete 43 | * database integrity, since everything is done is single transactions. 44 | * You may also override some of the hooks and methods if you wish to 45 | * customize how the adapter saves or retrieves data. 46 | * 47 | * @class EmberGraphAdapter 48 | * @extends Adapter 49 | * @category abstract 50 | */ 51 | var EmberGraphAdapter = Adapter.extend({ 52 | 53 | /** 54 | * Since we control both the client and 'server', we'll 55 | * use the same serializer for all records. 56 | * 57 | * @property serializer 58 | * @type JSONSerializer 59 | * @protected 60 | * @final 61 | */ 62 | serializer: computed({ 63 | get() { 64 | return Ember.getOwner(this).lookup('serializer:ember_graph'); 65 | } 66 | }), 67 | 68 | createRecord: function(record) { 69 | var _this = this; 70 | var typeKey = record.get('typeKey'); 71 | var serializerOptions = { requestType: 'createRecord', recordType: typeKey }; 72 | var json = this.serialize(record, serializerOptions); 73 | 74 | return this.serverCreateRecord(typeKey, json).then(function(payload) { 75 | return _this.deserialize(payload, serializerOptions); 76 | }); 77 | }, 78 | 79 | findRecord: function(typeKey, id) { 80 | var _this = this; 81 | var serializerOptions = { requestType: 'findRecord', recordType: typeKey }; 82 | 83 | return this.serverFindRecord(typeKey, id).then(function(payload) { 84 | return _this.deserialize(payload, serializerOptions); 85 | }); 86 | }, 87 | 88 | findMany: function(typeKey, ids) { 89 | var _this = this; 90 | var serializerOptions = { requestType: 'findMany', recordType: typeKey }; 91 | 92 | return this.serverFindMany(typeKey, ids).then(function(payload) { 93 | return _this.deserialize(payload, serializerOptions); 94 | }); 95 | }, 96 | 97 | findAll: function(typeKey) { 98 | var _this = this; 99 | var serializerOptions = { requestType: 'findAll', recordType: typeKey }; 100 | 101 | return this.serverFindAll(typeKey).then(function(payload) { 102 | return _this.deserialize(payload, serializerOptions); 103 | }); 104 | }, 105 | 106 | findQuery: function() { 107 | return Promise.reject('LocalStorageAdapter doesn\'t implement `findQuery` by default.'); 108 | }, 109 | 110 | updateRecord: function(record) { 111 | var _this = this; 112 | var typeKey = record.get('typeKey'); 113 | var serializerOptions = { requestType: 'updateRecord', recordType: typeKey }; 114 | var changes = this.serialize(record, serializerOptions); 115 | 116 | return this.serverUpdateRecord(typeKey, record.get('id'), changes).then(function(payload) { 117 | return _this.deserialize(payload, serializerOptions); 118 | }); 119 | }, 120 | 121 | deleteRecord: function(record) { 122 | var _this = this; 123 | var typeKey = record.get('typeKey'); 124 | var serializerOptions = { requestType: 'deleteRecord', recordType: typeKey }; 125 | 126 | return this.serverDeleteRecord(typeKey, record.get('id')).then(function(payload) { 127 | return _this.deserialize(payload, serializerOptions); 128 | }); 129 | }, 130 | 131 | serialize: function(record, options) { 132 | return this.get('serializer').serialize(record, options); 133 | }, 134 | 135 | deserialize: function(payload, options) { 136 | return this.get('serializer').deserialize(payload, options); 137 | } 138 | 139 | }); 140 | 141 | EmberGraphAdapter.reopen(LoadMethods); 142 | EmberGraphAdapter.reopen(ServerMethods); 143 | EmberGraphAdapter.reopen(DatabaseMethods); 144 | 145 | export default EmberGraphAdapter; 146 | -------------------------------------------------------------------------------- /src/util/inflector.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | /* 4 | I took the rules in this code from inflection.js, whose license can be found below. 5 | */ 6 | 7 | /* 8 | Copyright (c) 2010 Ryan Schuft (ryan.schuft@gmail.com) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | */ 28 | 29 | const uncountableWords = [ 30 | 'equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep', 'moose', 'deer', 'news' 31 | ]; 32 | 33 | const pluralRules = [ 34 | [/(m)an$/gi, '$1en'], 35 | [/(pe)rson$/gi, '$1ople'], 36 | [/(child)$/gi, '$1ren'], 37 | [/^(ox)$/gi, '$1en'], 38 | [/(ax|test)is$/gi, '$1es'], 39 | [/(octop|vir)us$/gi, '$1i'], 40 | [/(alias|status)$/gi, '$1es'], 41 | [/(bu)s$/gi, '$1ses'], 42 | [/(buffal|tomat|potat)o$/gi, '$1oes'], 43 | [/([ti])um$/gi, '$1a'], 44 | [/sis$/gi, 'ses'], 45 | [/(?:([^f])fe|([lr])f)$/gi, '$1$2ves'], 46 | [/(hive)$/gi, '$1s'], 47 | [/([^aeiouy]|qu)y$/gi, '$1ies'], 48 | [/(x|ch|ss|sh)$/gi, '$1es'], 49 | [/(matr|vert|ind)ix|ex$/gi, '$1ices'], 50 | [/([m|l])ouse$/gi, '$1ice'], 51 | [/(quiz)$/gi, '$1zes'], 52 | [/s$/gi, 's'], 53 | [/$/gi, 's'] 54 | ]; 55 | 56 | const singularRules = [ 57 | [/(m)en$/gi, '$1an'], 58 | [/(pe)ople$/gi, '$1rson'], 59 | [/(child)ren$/gi, '$1'], 60 | [/([ti])a$/gi, '$1um'], 61 | [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/gi, '$1$2sis'], 62 | [/(hive)s$/gi, '$1'], 63 | [/(tive)s$/gi, '$1'], 64 | [/(curve)s$/gi, '$1'], 65 | [/([lr])ves$/gi, '$1f'], 66 | [/([^fo])ves$/gi, '$1fe'], 67 | [/([^aeiouy]|qu)ies$/gi, '$1y'], 68 | [/(s)eries$/gi, '$1eries'], 69 | [/(m)ovies$/gi, '$1ovie'], 70 | [/(x|ch|ss|sh)es$/gi, '$1'], 71 | [/([m|l])ice$/gi, '$1ouse'], 72 | [/(bus)es$/gi, '$1'], 73 | [/(o)es$/gi, '$1'], 74 | [/(shoe)s$/gi, '$1'], 75 | [/(cris|ax|test)es$/gi, '$1is'], 76 | [/(octop|vir)i$/gi, '$1us'], 77 | [/(alias|status)es$/gi, '$1'], 78 | [/^(ox)en/gi, '$1'], 79 | [/(vert|ind)ices$/gi, '$1ex'], 80 | [/(matr)ices$/gi, '$1ix'], 81 | [/(quiz)zes$/gi, '$1'], 82 | [/s$/gi, ''] 83 | ]; 84 | 85 | const apply = function(str, rules) { 86 | if (uncountableWords.indexOf(str) >= 0) { 87 | return str; 88 | } 89 | 90 | for (let i = 0; i < rules.length; i = i + 1) { 91 | if (str.match(rules[i][0])) { 92 | return str.replace(rules[i][0], rules[i][1]); 93 | } 94 | } 95 | 96 | return str; 97 | }; 98 | 99 | const PLURALIZE_CACHE = {}; 100 | function pluralize(str) { 101 | if (!PLURALIZE_CACHE[str]) { 102 | PLURALIZE_CACHE[str] = apply(str, pluralRules); 103 | } 104 | 105 | return PLURALIZE_CACHE[str]; 106 | } 107 | 108 | const SINGULARIZE_CACHE = {}; 109 | function singularize(str) { 110 | if (!SINGULARIZE_CACHE[str]) { 111 | SINGULARIZE_CACHE[str] = apply(str, singularRules); 112 | } 113 | 114 | return SINGULARIZE_CACHE[str]; 115 | } 116 | 117 | function overridePluralRule(singular, plural) { 118 | PLURALIZE_CACHE[singular] = plural; 119 | } 120 | 121 | function overrideSingularRule(plural, singular) { 122 | SINGULARIZE_CACHE[plural] = singular; 123 | } 124 | 125 | if (Ember.ENV.EXTEND_PROTOTYPES === true || Ember.ENV.EXTEND_PROTOTYPES.String) { 126 | String.prototype.pluralize = String.prototype.pluralize || function() { 127 | return pluralize(this); 128 | }; 129 | 130 | String.prototype.singularize = String.prototype.singularize || function() { 131 | return singularize(this); 132 | }; 133 | } 134 | 135 | export { 136 | pluralize, 137 | singularize, 138 | overridePluralRule, 139 | overrideSingularRule 140 | }; 141 | -------------------------------------------------------------------------------- /tasks/convert_documentation_data.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 'use strict'; 3 | 4 | var fs = require('fs'); 5 | var marked = require('marked'); 6 | var renderer = new marked.Renderer(); 7 | var Handlebars = require('handlebars'); 8 | 9 | marked.setOptions({ 10 | renderer: renderer, 11 | highlight: function(code) { 12 | return require('highlight.js').highlightAuto(code).value; 13 | } 14 | }); 15 | 16 | module.exports = function(grunt) { 17 | grunt.registerTask('convert_documentation_data', function() { 18 | var json = JSON.parse(fs.readFileSync('doc/data.json', { encoding: 'utf8' })); 19 | 20 | var data = { 21 | namespaces: extractNamespaces(json), 22 | classes: extractClasses(json).map(function(klass) { 23 | klass.methods = extractClassMethods(json, getInheritanceChain(json, klass.name)); 24 | klass.properties = extractClassProperties(json, getInheritanceChain(json, klass.name)); 25 | return klass; 26 | }).filter(function(klass) { 27 | return klass.name !== 'EG'; 28 | }) 29 | }; 30 | 31 | fs.writeFileSync('doc/ember-graph.json', JSON.stringify(data)); 32 | }); 33 | }; 34 | 35 | function extractNamespaces(data) { 36 | var namespaces = []; 37 | 38 | data.classitems.forEach(function(item) { 39 | if (!item.namespace) { 40 | return; 41 | } 42 | 43 | if (namespaces.indexOf(item.namespace) < 0) { 44 | namespaces.push(item.namespace); 45 | } 46 | }); 47 | 48 | return namespaces.sort().map(function(namespace) { 49 | return extractNamespace(data, namespace); 50 | }); 51 | } 52 | 53 | function extractNamespace(data, namespace) { 54 | return { 55 | name: namespace, 56 | description: '', 57 | methods: extractNamespaceMethods(data, namespace), 58 | properties: extractNamespaceProperties(data, namespace) 59 | }; 60 | } 61 | 62 | function extractNamespaceMethods(data, namespace) { 63 | return extractNamespaceItems(data, namespace, 'method'); 64 | } 65 | 66 | function extractNamespaceProperties(data, namespace) { 67 | return extractNamespaceItems(data, namespace, 'property'); 68 | } 69 | 70 | function extractNamespaceItems(data, namespace, type) { 71 | return data.classitems.filter(function(item) { 72 | return (item.itemtype === type && item.namespace === namespace); 73 | }).map(function(item) { 74 | item.class = null; 75 | return (type === 'method' ? convertMethodItem : convertPropertyItem)(item); 76 | }).sort(function(a, b) { 77 | return (a.name < b.name ? -1 : 1); 78 | }); 79 | } 80 | 81 | function extractClasses(data) { 82 | return Object.keys(data.classes).sort().filter(function(className) { 83 | return !data.classes[className].namespace; 84 | }).map(function(className) { 85 | return convertClassItem(data.classes[className]); 86 | }); 87 | } 88 | 89 | function getInheritanceChain(data, className) { 90 | var superClass = data.classes[className].extends; 91 | 92 | if (data.classes[superClass]) { 93 | var chain = getInheritanceChain(data, superClass); 94 | chain.push(className); 95 | return chain; 96 | } else { 97 | return [className]; 98 | } 99 | } 100 | 101 | function extractClassMethods(data, classChain, methods) { 102 | return extractClassItems(data, classChain, 'method', {}); 103 | } 104 | 105 | function extractClassProperties(data, classChain) { 106 | return extractClassItems(data, classChain, 'property', {}); 107 | } 108 | 109 | function extractClassItems(data, classChain, type, items) { 110 | if (classChain.length <= 0) { 111 | return Object.keys(items).sort().map(function(key) { 112 | return items[key]; 113 | }); 114 | } 115 | 116 | var classItems = data.classitems.filter(function(item) { 117 | return (item.itemtype === type && item.class === classChain[0]); 118 | }); 119 | 120 | classItems.forEach(function(item) { 121 | items[item.name] = (type === 'method' ? convertMethodItem : convertPropertyItem)(item); 122 | }); 123 | 124 | var chainItems = extractClassItems(data, classChain.slice(1), type, items); 125 | return chainItems.filter(function(item) { 126 | return (item.private === false || item.defined_in === classChain[classChain.length - 1]); 127 | }); 128 | } 129 | 130 | function convertClassItem(item) { 131 | return { 132 | name: item.name, 133 | 'extends': item.extends || null, 134 | uses: item.uses || [], 135 | description: templateAndParseText(item.description || ''), 136 | deprecated: item.deprecated === true, 137 | file: { 138 | path: item.file, 139 | line: item.line 140 | } 141 | }; 142 | } 143 | 144 | function convertMethodItem(item) { 145 | return { 146 | name: item.name, 147 | description: templateAndParseText(item.description || ''), 148 | 'static': item.static === 1, 149 | deprecated: item.deprecated === true, 150 | parameters: (item.params || []).map(function(param) { 151 | param.description = templateAndParseText(param.description || ''); 152 | return param; 153 | }), 154 | 'return': item.return, 155 | defined_in: item.class, 156 | 'public': (item.access !== 'protected' && item.access !== 'private'), 157 | 'protected': item.access === 'protected', 158 | 'private': item.access === 'private', 159 | abstract: (item.category || []).indexOf('abstract') >= 0, 160 | file: { 161 | path: item.file, 162 | line: item.line 163 | } 164 | }; 165 | } 166 | 167 | function convertPropertyItem(item) { 168 | return { 169 | name: item.name, 170 | description: templateAndParseText(item.description || ''), 171 | type: item.type, 172 | 'static': item.static === 1, 173 | deprecated: item.deprecated === true, 174 | readOnly: item.final === 1, 175 | 'default': item.default, 176 | defined_in: item.class, 177 | 'public': (item.access !== 'protected' && item.access !== 'private'), 178 | 'protected': item.access === 'protected', 179 | 'private': item.access === 'private', 180 | file: { 181 | path: item.file, 182 | line: item.line 183 | } 184 | }; 185 | } 186 | 187 | function templateAndParseText(text) { 188 | text = text || ''; 189 | return marked(Handlebars.compile(text)()); 190 | } 191 | /* eslint-enable camelcase */ --------------------------------------------------------------------------------