├── .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 |
5 | {{#each properties}}
6 | -
7 | {{name}}
8 |
9 | {{/each}}
10 |
11 | {{/if}}
12 |
13 | {{#if methods}}
14 | Methods
15 |
16 |
17 | {{#each methods}}
18 | -
19 | {{name}}
20 |
21 | {{/each}}
22 |
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 |
2 | -
3 |
4 | Namespaces
5 |
6 |
7 | {{#each namespaces}}
8 | -
9 |
10 | {{name}}
11 |
12 |
13 | {{/each}}
14 |
15 |
16 |
17 | -
18 |
19 | Classes
20 |
21 |
22 | {{#each classes}}
23 | -
24 |
25 | {{name}}
26 |
27 |
28 | {{/each}}
29 |
--------------------------------------------------------------------------------
/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 |
15 |
16 |
36 |
37 |
{{{description}}}
38 | {{/with}}
39 | {{/if}}
40 |
41 | {{#if namespace}}
42 | {{#with namespace}}
43 |
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 |
43 | {{#each parameters}}
44 | -
45 | {{name}}
46 |
47 | {{{type}}}
48 | {{#if description}}
49 | {{strip-outer-paragraph description}}
50 | {{/if}}
51 |
52 |
53 | {{/each}}
54 |
55 | {{/if}}
56 |
57 | {{#if return}}
58 |
Returns:
59 |
60 | -
61 |
62 | {{{return.type}}}
63 | {{#if return.description}}
64 | {{strip-outer-paragraph return.description}}
65 | {{/if}}
66 |
67 |
68 |
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 */
--------------------------------------------------------------------------------