├── app
├── .gitkeep
└── initializers
│ ├── syncer.js
│ ├── local-store.js
│ └── extend-ds-model.js
├── addon
├── .gitkeep
├── utils
│ ├── is-object.js
│ ├── is-model-instance.js
│ ├── backup.js
│ ├── generate-unique-id.js
│ └── reload-local-records.js
├── model.js
├── initializers
│ ├── local-store.js
│ ├── extend-ds-model.js
│ └── syncer.js
├── stores
│ ├── local-store.js
│ ├── main-store
│ │ ├── decorate-api-call.js
│ │ ├── decorate-serializer.js
│ │ └── decorate-adapter.js
│ └── main-store.js
└── syncer.js
├── vendor
└── .gitkeep
├── tests
├── unit
│ └── .gitkeep
├── dummy
│ ├── app
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ ├── .gitkeep
│ │ │ ├── animal.js
│ │ │ ├── team.js
│ │ │ ├── job.js
│ │ │ └── user.js
│ │ ├── routes
│ │ │ ├── .gitkeep
│ │ │ ├── index.js
│ │ │ ├── fetch-by-id.js
│ │ │ ├── reload-record.js
│ │ │ ├── fetch-all.js
│ │ │ ├── update.js
│ │ │ ├── user-update-with-id.js
│ │ │ ├── delete.js
│ │ │ ├── find-by-id-private.js
│ │ │ ├── fetch-all-jobs.js
│ │ │ ├── create.js
│ │ │ ├── application.js
│ │ │ └── create-job.js
│ │ ├── styles
│ │ │ └── app.css
│ │ ├── views
│ │ │ └── .gitkeep
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ └── .gitkeep
│ │ ├── templates
│ │ │ ├── components
│ │ │ │ └── .gitkeep
│ │ │ ├── fetch-by-id.hbs
│ │ │ ├── reload-record.hbs
│ │ │ ├── find-by-id-private.hbs
│ │ │ ├── create.hbs
│ │ │ ├── delete.hbs
│ │ │ ├── update.hbs
│ │ │ ├── user-update-with-id.hbs
│ │ │ ├── fetch-all.hbs
│ │ │ ├── fetch-all-jobs.hbs
│ │ │ ├── create-job.hbs
│ │ │ └── application.hbs
│ │ ├── serializers
│ │ │ └── animal.js
│ │ ├── store.js
│ │ ├── app.js
│ │ ├── index.html
│ │ ├── router.js
│ │ └── initializers
│ │ │ └── reopen-syncer.js
│ ├── public
│ │ ├── robots.txt
│ │ └── crossdomain.xml
│ └── config
│ │ └── environment.js
├── test-helper.js
├── helpers
│ ├── resolver.js
│ └── start-app.js
├── .jshintrc
├── index.html
└── acceptance
│ ├── user-reload-record-test.js
│ ├── user-fetch-by-id-test.js
│ ├── user-find-by-id-private-test.js
│ ├── syncer-reset-test.js
│ ├── user-create-delete-test.js
│ ├── user-create-test.js
│ ├── animal-create-test.js
│ ├── user-fetch-all-test.js
│ ├── user-delete-test.js
│ ├── user-update-test.js
│ ├── user-create-update-test.js
│ ├── team-create-test.js
│ └── job-create-test.js
├── server
├── .jshintrc
├── index.js
└── mocks
│ ├── teams.js
│ ├── jobs.js
│ ├── animals.js
│ └── users.js
├── .bowerrc
├── index.js
├── config
└── environment.js
├── blueprints
├── application-store
│ ├── files
│ │ └── app
│ │ │ └── store.js
│ └── index.js
└── reopen-syncer-initializer
│ ├── index.js
│ └── files
│ └── app
│ └── initializers
│ └── reopen-syncer.js
├── .npmignore
├── testem.json
├── .ember-cli
├── .gitignore
├── .travis.yml
├── bower.json
├── .editorconfig
├── .jshintrc
├── Brocfile.js
├── LICENSE.md
├── LICENSE
├── package.json
└── README.md
/app/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/views/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "analytics": false
4 | }
5 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true */
2 | 'use strict';
3 |
4 | module.exports = {
5 | name: 'ember-fryctoria'
6 | };
7 |
--------------------------------------------------------------------------------
/addon/utils/is-object.js:
--------------------------------------------------------------------------------
1 | export default function isObject(val) {
2 | return val !== null && typeof val === 'object';
3 | }
4 |
--------------------------------------------------------------------------------
/config/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(/* environment, appConfig */) {
4 | return { };
5 | };
6 |
--------------------------------------------------------------------------------
/tests/dummy/app/serializers/animal.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 |
3 | export default DS.ActiveModelSerializer.extend({
4 | });
5 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/fetch-by-id.hbs:
--------------------------------------------------------------------------------
1 |
fetchById
2 | {{model.id}}
3 | {{model.name}}
4 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import resolver from './helpers/resolver';
2 | import { setResolver } from 'ember-mocha';
3 |
4 | setResolver(resolver);
5 |
--------------------------------------------------------------------------------
/addon/utils/is-model-instance.js:
--------------------------------------------------------------------------------
1 | export default function isModelInstance(val) {
2 | return val && val.get && val.get('constructor.typeKey');
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/reload-record.hbs:
--------------------------------------------------------------------------------
1 | Reload Record
2 | {{model.id}}
3 | {{model.name}}
4 |
5 |
--------------------------------------------------------------------------------
/tests/dummy/app/store.js:
--------------------------------------------------------------------------------
1 | import FryctoriaMainStore from 'ember-fryctoria/stores/main-store';
2 |
3 | export default FryctoriaMainStore.extend({
4 | });
5 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/find-by-id-private.hbs:
--------------------------------------------------------------------------------
1 | findById(Private)
2 | {{model.id}}
3 | {{model.name}}
4 |
5 |
--------------------------------------------------------------------------------
/blueprints/application-store/files/app/store.js:
--------------------------------------------------------------------------------
1 | import FryctoriaMainStore from 'ember-fryctoria/stores/main-store';
2 |
3 | export default FryctoriaMainStore.extend({
4 | });
5 |
6 |
--------------------------------------------------------------------------------
/blueprints/reopen-syncer-initializer/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | normalizeEntityName: function() {},
3 | description: 'Generates a initializer to customize syncer.'
4 | };
5 |
6 |
--------------------------------------------------------------------------------
/blueprints/application-store/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | normalizeEntityName: function() {},
3 | description: 'Generates application store which extends from FryctoriaStore.'
4 | };
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | tests/
3 |
4 | .bowerrc
5 | .editorconfig
6 | .ember-cli
7 | .travis.yml
8 | .npmignore
9 | **/.gitkeep
10 | bower.json
11 | Brocfile.js
12 | testem.json
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/animal.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 |
3 | var Animal = DS.Model.extend({
4 | nickName: DS.attr('string'),
5 | });
6 |
7 | export default Animal;
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/team.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 |
3 | var Team = DS.Model.extend({
4 | name: DS.attr('string'),
5 | users: DS.hasMany('user')
6 | });
7 |
8 | export default Team;
9 |
--------------------------------------------------------------------------------
/addon/model.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 | import decorateAPICall from './stores/main-store/decorate-api-call';
3 |
4 | export default DS.Model.extend({
5 | save: decorateAPICall('single')
6 | });
7 |
--------------------------------------------------------------------------------
/testem.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "qunit",
3 | "test_page": "tests/index.html?hidepassed",
4 | "launch_in_ci": [
5 | "PhantomJS"
6 | ],
7 | "launch_in_dev": [
8 | "PhantomJS",
9 | "Chrome"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/app/initializers/syncer.js:
--------------------------------------------------------------------------------
1 | import initializer from 'ember-fryctoria/initializers/syncer';
2 | import { initialize } from 'ember-fryctoria/initializers/syncer';
3 |
4 | export { initialize as initialize };
5 | export default initializer;
6 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/index.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | // model: function() {
5 | // return this.store.syncer.runAllJobs();
6 | // }
7 | });
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/job.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 |
3 | var Job = DS.Model.extend({
4 | user: DS.belongsTo('user'),
5 | name: DS.attr('string'),
6 | salary: DS.attr('number')
7 | });
8 |
9 | export default Job;
10 |
11 |
--------------------------------------------------------------------------------
/app/initializers/local-store.js:
--------------------------------------------------------------------------------
1 | import initializer from 'ember-fryctoria/initializers/local-store';
2 | import { initialize } from 'ember-fryctoria/initializers/local-store';
3 |
4 | export { initialize as initialize };
5 | export default initializer;
6 |
7 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/create.hbs:
--------------------------------------------------------------------------------
1 | create
2 | {{#if model.isSaved}}
3 | created
4 | {{/if}}
5 | {{input value=model.name id='name'}}
6 | {{input value=model.age id='age'}}
7 |
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/delete.hbs:
--------------------------------------------------------------------------------
1 | delete
2 | {{#if model.isDeleted}}
3 | deleted
4 | {{/if}}
5 | {{model.id}}
6 | {{model.name}}
7 |
8 |
--------------------------------------------------------------------------------
/app/initializers/extend-ds-model.js:
--------------------------------------------------------------------------------
1 | import initializer from 'ember-fryctoria/initializers/extend-ds-model';
2 | import { initialize } from 'ember-fryctoria/initializers/extend-ds-model';
3 |
4 | export { initialize as initialize };
5 | export default initializer;
6 |
7 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/user.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 |
3 | var User = DS.Model.extend({
4 | name: DS.attr('string'),
5 | age: DS.attr('number'),
6 | isActive: DS.attr('boolean', {defaultValue: false}),
7 | team: DS.belongsTo('team'),
8 | });
9 |
10 | export default User;
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/fetch-by-id.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function(params) {
5 | var firstUserId = this.store.all('user').get('firstObject.id');
6 | return this.store.fetchById('user', firstUserId);
7 | }
8 | });
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/reload-record.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | var firstUser = this.store.all('user').get('firstObject');
6 | return firstUser.reload(); // this makes a call to store#reloadRecord
7 | }
8 | });
9 |
10 |
11 |
--------------------------------------------------------------------------------
/addon/utils/backup.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default function backup(isOffline, backupFn, args) {
4 | return function(error) {
5 | if(isOffline(error)) {
6 | return backupFn.apply(null, args);
7 | } else {
8 | return Ember.RSVP.reject(error);
9 | }
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/fetch-all.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | return this.store.fetchAll('user');
6 | },
7 |
8 | actions: {
9 | delete: function(model) {
10 | model.destroyRecord();
11 | }
12 | }
13 | });
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/update.hbs:
--------------------------------------------------------------------------------
1 | update
2 |
3 | {{#if model.isDirty}}
4 | isDirty
5 | {{else}}
6 | isClean
7 | {{/if}}
8 |
9 | {{input value=model.name id='name'}}
10 | {{input value=model.age id='age'}}
11 |
12 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": false
9 | }
10 |
--------------------------------------------------------------------------------
/tests/helpers/resolver.js:
--------------------------------------------------------------------------------
1 | import Resolver from 'ember/resolver';
2 | import config from '../../config/environment';
3 |
4 | var resolver = Resolver.create();
5 |
6 | resolver.namespace = {
7 | modulePrefix: config.modulePrefix,
8 | podModulePrefix: config.podModulePrefix
9 | };
10 |
11 | export default resolver;
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # misc
12 | /.sass-cache
13 | /connect.lock
14 | /coverage/*
15 | /libpeerconnection.log
16 | npm-debug.log
17 | testem.log
18 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/update.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | return this.store.all('user').get('firstObject');
6 | },
7 |
8 | actions: {
9 | update: function() {
10 | this.get('controller.model').save();
11 | }
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 |
4 | sudo: false
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | before_install:
11 | - "npm config set spin false"
12 | - "npm install -g npm@^2"
13 |
14 | install:
15 | - npm install -g bower
16 | - npm install
17 | - bower install
18 |
19 | script:
20 | - npm test
21 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/user-update-with-id.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function(params) {
5 | return this.store.getById('user', params.id);
6 | },
7 |
8 | actions: {
9 | update: function() {
10 | this.get('controller.model').save();
11 | }
12 | }
13 | });
14 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/user-update-with-id.hbs:
--------------------------------------------------------------------------------
1 | user update with id
2 |
3 | {{#if model.isDirty}}
4 | isDirty
5 | {{else}}
6 | isClean
7 | {{/if}}
8 |
9 | {{input value=model.name id='name'}}
10 | {{input value=model.age id='age'}}
11 |
12 |
--------------------------------------------------------------------------------
/addon/initializers/local-store.js:
--------------------------------------------------------------------------------
1 | import FryctoriaLocalStore from '../stores/local-store';
2 |
3 | export function initialize(container, application) {
4 | application.register('store:local', FryctoriaLocalStore);
5 | }
6 |
7 | export default {
8 | name: 'local-store',
9 | before: 'syncer',
10 | initialize: initialize
11 | };
12 |
--------------------------------------------------------------------------------
/addon/initializers/extend-ds-model.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 | import FryctoriaModel from '../model';
3 |
4 | export function initialize(/* container, application */) {
5 | DS.set('Model', FryctoriaModel);
6 | }
7 |
8 | export default {
9 | name: 'extend-ds-model',
10 | before: 'store',
11 | initialize: initialize
12 | };
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/fetch-all.hbs:
--------------------------------------------------------------------------------
1 | fetchAll
2 |
3 | {{#each user in model}}
4 | -
5 | {{user.id}}, {{user.name}}, {{user.age}},
6 | {{link-to 'Update' 'user-update-with-id' user.id class='update'}}
7 | Delete
8 |
9 | {{/each}}
10 |
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/delete.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | return this.store.all('user').get('firstObject');
6 | },
7 |
8 | actions: {
9 | delete: function() {
10 | this.get('controller.model').destroyRecord();
11 | }
12 | }
13 | });
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/find-by-id-private.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | var firstUser = this.store.all('user').get('firstObject');
6 | // force ember data to talk to remote server
7 | firstUser.unloadRecord();
8 | return this.store.findById('user', firstUser.get('id'));
9 | }
10 | });
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/fetch-all-jobs.hbs:
--------------------------------------------------------------------------------
1 | fetchAll jobs
2 |
3 | {{#each job in model}}
4 | -
5 | {{job.id}}, {{job.name}}, {{job.salary}}, (USER is {{job.user.name}})
6 |
7 | Delete
8 |
9 | {{/each}}
10 |
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/fetch-all-jobs.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | var store = this.store;
6 | return store.fetchAll('user').then(function() {
7 | return store.fetchAll('job');
8 | });
9 | },
10 |
11 | actions: {
12 | delete: function(model) {
13 | model.destroyRecord();
14 | }
15 | }
16 | });
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/create-job.hbs:
--------------------------------------------------------------------------------
1 | job create
2 | {{#if model.isSaved}}
3 | created
4 | {{/if}}
5 | {{input value=model.name id ='name'}}
6 | {{input value=model.salary id ='salary'}}
7 | {{view "select" id='user'
8 | content=users
9 | value=model.user
10 | optionValuePath="content.user"
11 | optionLabelPath="content.name"}}
12 |
13 |
--------------------------------------------------------------------------------
/addon/utils/generate-unique-id.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @return { String } a combination of timestamp and 5 random digits
3 | */
4 | export default function generateUniqueId(prefix) {
5 | var time, randomFiveDigits;
6 |
7 | prefix = prefix || 'fryctoria';
8 | time = (new Date()).getTime();
9 | randomFiveDigits = Math.random().toFixed(5).slice(2);
10 | return [prefix, time, randomFiveDigits].join('-');
11 | }
12 |
13 |
14 |
--------------------------------------------------------------------------------
/addon/initializers/syncer.js:
--------------------------------------------------------------------------------
1 | import Syncer from '../syncer';
2 |
3 | export function initialize(container, application) {
4 | application.register('syncer:main', Syncer);
5 | application.inject('store:main', 'syncer', 'syncer:main');
6 | application.inject('model', 'syncer', 'syncer:main');
7 | }
8 |
9 | export default {
10 | name: 'syncer',
11 | before: 'store',
12 | after: 'extend-ds-model',
13 | initialize: initialize
14 | };
15 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Resolver from 'ember/resolver';
3 | import loadInitializers from 'ember/load-initializers';
4 | import config from './config/environment';
5 |
6 | Ember.MODEL_FACTORY_INJECTIONS = true;
7 |
8 | var App = Ember.Application.extend({
9 | modulePrefix: config.modulePrefix,
10 | podModulePrefix: config.podModulePrefix,
11 | Resolver: Resolver
12 | });
13 |
14 | loadInitializers(App, config.modulePrefix);
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-fryctoria",
3 | "dependencies": {
4 | "jquery": "^1.11.1",
5 | "ember": "1.10.0",
6 | "ember-data": "1.0.0-beta.16.1",
7 | "ember-resolver": "~0.1.12",
8 | "loader.js": "ember-cli/loader.js#3.2.0",
9 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3",
10 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3",
11 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.0.2",
12 | "ember-mocha": "~0.5.1",
13 | "localforage": "~1.2.1",
14 | "jquery-mockjax": "~1.6.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/create.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | var newName = 'user' + ( Math.random() * 1000 ).toFixed();
6 | return this.store.createRecord('user', {name: newName});
7 | },
8 |
9 | actions: {
10 | create: function() {
11 | this.get('controller.model').save();
12 | },
13 | willTransition: function() {
14 | var model = this.get('controller.model');
15 | if(model.get('isNew')) {
16 | model.deleteRecord();
17 | } else {
18 | model.rollback();
19 | }
20 | }
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.js]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.hbs]
21 | indent_style = space
22 | indent_size = 2
23 |
24 | [*.css]
25 | indent_style = space
26 | indent_size = 2
27 |
28 | [*.html]
29 | indent_style = space
30 | indent_size = 2
31 |
32 | [*.{diff,md}]
33 | trim_trailing_whitespace = false
34 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "-Promise",
6 | "andLater"
7 | ],
8 | "browser": true,
9 | "boss": true,
10 | "curly": true,
11 | "debug": false,
12 | "devel": true,
13 | "eqeqeq": true,
14 | "evil": true,
15 | "forin": false,
16 | "immed": false,
17 | "laxbreak": false,
18 | "newcap": true,
19 | "noarg": true,
20 | "noempty": false,
21 | "nonew": false,
22 | "nomen": false,
23 | "onevar": false,
24 | "plusplus": false,
25 | "regexp": false,
26 | "undef": true,
27 | "sub": true,
28 | "strict": false,
29 | "white": false,
30 | "eqnull": true,
31 | "esnext": true,
32 | "unused": true
33 | }
34 |
--------------------------------------------------------------------------------
/tests/dummy/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy
7 |
8 |
9 |
10 | {{content-for 'head'}}
11 |
12 |
13 |
14 |
15 | {{content-for 'head-footer'}}
16 |
17 |
18 | {{content-for 'body'}}
19 |
20 |
21 |
22 |
23 | {{content-for 'body-footer'}}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import config from './config/environment';
3 |
4 | var Router = Ember.Router.extend({
5 | location: config.locationType
6 | });
7 |
8 | Router.map(function() {
9 | this.route('fetch-all');
10 | this.route('fetch-by-id');
11 | this.route('find-by-id-private');
12 | this.route('reload-record');
13 | // This may not be supported by an adapter, implement later
14 | // this.route('findQuery');
15 | this.route('update');
16 | this.route('user-update-with-id', { path: 'user-update-with-id/:id' });
17 | this.route('create');
18 | this.route('delete');
19 |
20 | // belongsTo relationship
21 | this.route('fetch-all-jobs');
22 | this.route('create-job');
23 | });
24 |
25 | export default Router;
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 | Ember Fryctoria
2 |
3 |
10 |
11 | User (Single CRUD)
12 |
13 | {{link-to 'Index' 'index'}}
14 | {{link-to 'fetchAll' 'fetch-all'}}
15 | {{link-to 'fetchById' 'fetch-by-id'}}
16 | {{link-to 'findById(Private)' 'find-by-id-private'}}
17 | {{link-to 'reloadRecord' 'reload-record'}}
18 | {{link-to 'update' 'update'}}
19 | {{link-to 'create' 'create'}}
20 | {{link-to 'delete' 'delete'}}
21 |
22 |
23 | Job (belongsTo User)
24 |
25 | {{link-to 'fetchAll Users & Jobs' 'fetch-all-jobs'}}
26 | {{link-to 'create-job' 'create-job'}}
27 |
28 |
29 | {{outlet}}
30 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/application.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import ENV from 'dummy/config/environment';
3 |
4 | export default Ember.Route.extend({
5 | setupController: function(ctrl) {
6 | ctrl.set('isOnline', true);
7 | },
8 |
9 | actions: {
10 | toggleOnline: function() {
11 | var ctrl = this.get('controller');
12 | var isOnline = ctrl.get('isOnline');
13 |
14 | if(isOnline) {
15 | var mockId = Ember.$.mockjax({
16 | status: 0,
17 | url: /.*/,
18 | responseTime: 0,
19 | });
20 | ctrl.set('mockId', mockId);
21 | } else {
22 | Ember.$.mockjax.clear(ctrl.get('mockId'));
23 | }
24 |
25 | ctrl.set('isOnline', !isOnline);
26 | }
27 | }
28 | });
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | // To use it create some files under `mocks/`
2 | // e.g. `server/mocks/ember-hamsters.js`
3 | //
4 | // module.exports = function(app) {
5 | // app.get('/ember-hamsters', function(req, res) {
6 | // res.send('hello');
7 | // });
8 | // };
9 | var bodyParser = require('body-parser');
10 |
11 | module.exports = function(app) {
12 | var globSync = require('glob').sync;
13 | var mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require);
14 | var proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require);
15 |
16 | // Log proxy requests
17 | var morgan = require('morgan');
18 | app.use(morgan('dev'));
19 | app.use(bodyParser.json());
20 |
21 | mocks.forEach(function(route) { route(app); });
22 | proxies.forEach(function(route) { route(app); });
23 |
24 | };
25 |
--------------------------------------------------------------------------------
/Brocfile.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true */
2 | /* global require, module */
3 |
4 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
5 |
6 | var app = new EmberAddon();
7 |
8 | // Use `app.import` to add additional libraries to the generated
9 | // output files.
10 | //
11 | // If you need to use different assets in different
12 | // environments, specify an object as the first parameter. That
13 | // object's keys should be the environment name and the values
14 | // should be the asset to use in that environment.
15 | //
16 | // If the library that you are including contains AMD or ES6
17 | // modules that you would like to import into your application
18 | // please specify an object with the list of modules as keys
19 | // along with the exports of each module as its value.
20 | app.import('bower_components/jquery-mockjax/jquery.mockjax.js');
21 |
22 | module.exports = app.toTree();
23 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/tests/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "predef": [
3 | "document",
4 | "window",
5 | "location",
6 | "setTimeout",
7 | "$",
8 | "-Promise",
9 | "define",
10 | "console",
11 | "visit",
12 | "exists",
13 | "fillIn",
14 | "click",
15 | "keyEvent",
16 | "triggerEvent",
17 | "find",
18 | "findWithAssert",
19 | "wait",
20 | "DS",
21 | "andThen",
22 | "andLater",
23 | "currentURL",
24 | "currentPath",
25 | "currentRouteName",
26 | "setOnlineStatus"
27 | ],
28 | "node": false,
29 | "browser": false,
30 | "boss": true,
31 | "curly": false,
32 | "debug": false,
33 | "devel": false,
34 | "eqeqeq": true,
35 | "evil": true,
36 | "forin": false,
37 | "immed": false,
38 | "laxbreak": false,
39 | "newcap": true,
40 | "noarg": true,
41 | "noempty": false,
42 | "nonew": false,
43 | "nomen": false,
44 | "onevar": false,
45 | "plusplus": false,
46 | "regexp": false,
47 | "undef": true,
48 | "sub": true,
49 | "strict": false,
50 | "white": false,
51 | "eqnull": true,
52 | "esnext": true
53 | }
54 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dummy Tests
7 |
8 |
9 |
10 | {{content-for 'head'}}
11 | {{content-for 'test-head'}}
12 |
13 |
14 |
15 |
16 |
17 | {{content-for 'head-footer'}}
18 | {{content-for 'test-head-footer'}}
19 |
20 |
21 |
22 | {{content-for 'body'}}
23 | {{content-for 'test-body'}}
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{content-for 'body-footer'}}
31 | {{content-for 'test-body-footer'}}
32 |
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 yang2007chun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, 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,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/create-job.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export default Ember.Route.extend({
4 | model: function() {
5 | var newName = 'job' + ( Math.random() * 1000 ).toFixed();
6 | var newSalary = ( Math.random() * 1000 ).toFixed();
7 | var user = this.store.all('user').get('firstObject');
8 | return this.store.createRecord('job', {
9 | name: newName,
10 | salary: newSalary,
11 | user: user
12 | });
13 | },
14 |
15 | setupController: function(controller, model) {
16 | var users = this.store.all('user')
17 | .map(function(user) {
18 | return {
19 | user: user,
20 | name: user.get('name')
21 | };
22 | });
23 | controller.set('model', model);
24 | controller.set('users', users);
25 | },
26 |
27 | actions: {
28 | create: function() {
29 | this.get('controller.model').save();
30 | },
31 | willTransition: function() {
32 | var model = this.get('controller.model');
33 | if(model.get('isNew')) {
34 | model.deleteRecord();
35 | } else {
36 | model.rollback();
37 | }
38 | }
39 | }
40 | });
41 |
42 |
--------------------------------------------------------------------------------
/tests/dummy/app/initializers/reopen-syncer.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export function initialize(container, application) {
4 | var syncer = container.lookup('syncer:main');
5 |
6 | /*
7 | * Handle errors when tyring to syncUp.
8 | *
9 | * Default: undefined
10 | *
11 | * The following example remove all jobs and records in localforage
12 | * and restart your app.
13 | */
14 | // syncer.reopen({
15 | // handleSyncUpError: function(error) {
16 | // // delete all jobs and all records in localforage.
17 | // this.reset().then(function() {
18 | // // reload page.
19 | // window.location.reload();
20 | // });
21 | // }
22 | // });
23 |
24 | /*
25 | * Decide what is offline.
26 | *
27 | * Default:
28 | * ```
29 | * isOffline: function(error) {
30 | * return error && error.status === 0;
31 | * }
32 | * ```
33 | */
34 | // syncer.reopen({
35 | // isOffline: function(error) {
36 | // return error && error.status === 0;
37 | // }
38 | // });
39 | }
40 |
41 | export default {
42 | name: 'reopen-syncer',
43 | after: 'syncer',
44 | initialize: initialize
45 | };
46 |
47 |
--------------------------------------------------------------------------------
/blueprints/reopen-syncer-initializer/files/app/initializers/reopen-syncer.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | export function initialize(container, application) {
4 | var syncer = container.lookup('syncer:main');
5 |
6 | /*
7 | * Handle errors when tyring to syncUp.
8 | *
9 | * Default: undefined
10 | *
11 | * The following example remove all jobs and records in localforage
12 | * and restart your app.
13 | */
14 | // syncer.reopen({
15 | // handleSyncUpError: function(error) {
16 | // // delete all jobs and all records in localforage.
17 | // this.reset().then(function() {
18 | // // reload page.
19 | // window.location.reload();
20 | // });
21 | // }
22 | // });
23 |
24 | /*
25 | * Decide what is offline.
26 | *
27 | * Default:
28 | * ```
29 | * isOffline: function(error) {
30 | * return error && error.status === 0;
31 | * }
32 | * ```
33 | */
34 | // syncer.reopen({
35 | // isOffline: function(error) {
36 | // return error && error.status === 0;
37 | // }
38 | // });
39 | }
40 |
41 | export default {
42 | name: 'reopen-syncer',
43 | after: 'syncer',
44 | initialize: initialize
45 | };
46 |
--------------------------------------------------------------------------------
/server/mocks/teams.js:
--------------------------------------------------------------------------------
1 | var teams = [
2 | { id: 1, name: 'Hurricane' },
3 | { id: 2, name: 'Typhoon' },
4 | { id: 3, name: 'Tsunami' }
5 | ];
6 |
7 | module.exports = function(app) {
8 | var express = require('express');
9 | var teamsRouter = express.Router();
10 |
11 | teamsRouter.get('/', function(req, res) {
12 | res.send({
13 | 'teams': teams.filter(Boolean)
14 | });
15 | });
16 |
17 | teamsRouter.post('/', function(req, res) {
18 | var team = req.body.team;
19 | team.id = teams.length + 1;
20 | teams.push(team);
21 |
22 | res.status(201);
23 | res.send({
24 | 'teams': team
25 | });
26 | });
27 |
28 | teamsRouter.get('/:id', function(req, res) {
29 | res.send({
30 | 'teams': teams[+req.params.id - 1]
31 | });
32 | });
33 |
34 | teamsRouter.put('/:id', function(req, res) {
35 | var id = req.params.id;
36 | var team = req.body.team;
37 | team.id = id;
38 | teams[+id - 1] = team;
39 |
40 | res.send({
41 | 'teams': {
42 | id: req.params.id
43 | }
44 | });
45 | });
46 |
47 | teamsRouter.delete('/:id', function(req, res) {
48 | delete teams[+req.params.id - 1];
49 | res.status(204).end();
50 | });
51 |
52 | app.use('/teams', teamsRouter);
53 | };
54 |
55 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | /* jshint node: true */
2 |
3 | module.exports = function(environment) {
4 | var ENV = {
5 | modulePrefix: 'dummy',
6 | environment: environment,
7 | baseURL: '/',
8 | locationType: 'auto',
9 | EmberENV: {
10 | FEATURES: {
11 | // Here you can enable experimental features on an ember canary build
12 | // e.g. 'with-controller': true
13 | }
14 | },
15 |
16 | APP: {
17 | // Here you can pass flags/options to your application instance
18 | // when it is created
19 | }
20 | };
21 |
22 | if (environment === 'development') {
23 | // ENV.APP.LOG_RESOLVER = true;
24 | // ENV.APP.LOG_ACTIVE_GENERATION = true;
25 | // ENV.APP.LOG_TRANSITIONS = true;
26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
27 | // ENV.APP.LOG_VIEW_LOOKUPS = true;
28 | }
29 |
30 | if (environment === 'test') {
31 | // Testem prefers this...
32 | ENV.baseURL = '/';
33 | ENV.locationType = 'none';
34 |
35 | // keep test console output quieter
36 | ENV.APP.LOG_ACTIVE_GENERATION = false;
37 | ENV.APP.LOG_VIEW_LOOKUPS = false;
38 |
39 | ENV.APP.rootElement = '#ember-testing';
40 | }
41 |
42 | if (environment === 'production') {
43 |
44 | }
45 |
46 | return ENV;
47 | };
48 |
--------------------------------------------------------------------------------
/server/mocks/jobs.js:
--------------------------------------------------------------------------------
1 | module.exports = function(app) {
2 | var express = require('express');
3 | var jobsRouter = express.Router();
4 | var jobs = [
5 | {id: 1, name: 'Mechanical Engineer', salary: 100},
6 | {id: 2, name: 'Teacher', salary: 100},
7 | {id: 3, name: 'Web Developer', salary: 100}
8 | ];
9 |
10 | jobsRouter.get('/', function(req, res) {
11 | res.send({
12 | 'jobs': jobs.filter(Boolean)
13 | });
14 | });
15 |
16 | jobsRouter.post('/', function(req, res) {
17 | var job = req.body.job;
18 | job.id = jobs.length + 1;
19 | jobs.push(job);
20 |
21 | res.status(201);
22 | res.send({
23 | 'jobs': job
24 | });
25 | });
26 |
27 | jobsRouter.get('/:id', function(req, res) {
28 | res.send({
29 | 'jobs': jobs[+req.params.id - 1]
30 | });
31 | });
32 |
33 | jobsRouter.put('/:id', function(req, res) {
34 | var id = req.params.id;
35 | var job = req.body.job;
36 | job.id = id;
37 | jobs[+id - 1] = job;
38 |
39 | res.send({
40 | 'jobs': {
41 | id: req.params.id
42 | }
43 | });
44 | });
45 |
46 | jobsRouter.delete('/:id', function(req, res) {
47 | delete jobs[+req.params.id - 1];
48 | res.status(204).end();
49 | });
50 |
51 | app.use('/jobs', jobsRouter);
52 | };
53 |
--------------------------------------------------------------------------------
/tests/helpers/start-app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Application from '../../app';
3 | import Router from '../../router';
4 | import config from '../../config/environment';
5 |
6 | // register helper;
7 |
8 | Ember.Test.registerAsyncHelper('andLater',
9 | function(app, callback, timeout) {
10 | Ember.run(function() {
11 | Ember.run.later(function() {
12 | callback();
13 | }, timeout || 500);
14 | });
15 | }
16 | );
17 |
18 | var setOnlineStatusFn = function() {
19 | var mockId;
20 |
21 | return function(app, status) {
22 | if(status) {
23 | Ember.$.mockjax.clear(mockId);
24 | } else {
25 | mockId = Ember.$.mockjax({
26 | status: 0,
27 | url: /.*/,
28 | responseTime: 0,
29 | });
30 | }
31 | };
32 | }();
33 |
34 | Ember.Test.registerHelper('setOnlineStatus', setOnlineStatusFn);
35 |
36 | export default function startApp(attrs) {
37 | var application;
38 |
39 | var attributes = Ember.merge({}, config.APP);
40 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override;
41 |
42 | Ember.run(function() {
43 | application = Application.create(attributes);
44 | application.setupForTesting();
45 | application.injectTestHelpers();
46 | });
47 |
48 | return application;
49 | }
50 |
--------------------------------------------------------------------------------
/server/mocks/animals.js:
--------------------------------------------------------------------------------
1 | var animals = [
2 | { id: 1, nick_name: 'Lili' },
3 | { id: 2, nick_name: 'Xiaohu' },
4 | { id: 3, nick_name: 'Xiaobao'}
5 | ];
6 |
7 | module.exports = function(app) {
8 | var express = require('express');
9 | var animalsRouter = express.Router();
10 |
11 | animalsRouter.get('/', function(req, res) {
12 | res.send({
13 | 'animals': animals.filter(Boolean)
14 | });
15 | });
16 |
17 | animalsRouter.post('/', function(req, res) {
18 | var animal = req.body.animal;
19 | animal.id = animals.length + 1;
20 | animals.push(animal);
21 |
22 | res.status(201);
23 | res.send({
24 | 'animals': animal
25 | });
26 | });
27 |
28 | animalsRouter.get('/:id', function(req, res) {
29 | res.send({
30 | 'animals': animals[+req.params.id - 1]
31 | });
32 | });
33 |
34 | animalsRouter.put('/:id', function(req, res) {
35 | var id = req.params.id;
36 | var animal = req.body.animal;
37 | animal.id = id;
38 | animals[+id - 1] = animal;
39 |
40 | res.send({
41 | 'animals': {
42 | id: req.params.id
43 | }
44 | });
45 | });
46 |
47 | animalsRouter.delete('/:id', function(req, res) {
48 | delete animals[+req.params.id - 1];
49 | res.status(204).end();
50 | });
51 |
52 | app.use('/animals', animalsRouter);
53 | };
54 |
--------------------------------------------------------------------------------
/server/mocks/users.js:
--------------------------------------------------------------------------------
1 | var users = [
2 | { id: 1, name: 'Chun', age: 27, isActive: true },
3 | { id: 2, name: 'John', age: 20, isActive: true },
4 | { id: 3, name: 'Daniel', age: 18, isActive: false }
5 | ];
6 |
7 | module.exports = function(app) {
8 | var express = require('express');
9 | var usersRouter = express.Router();
10 |
11 | usersRouter.get('/', function(req, res) {
12 | res.send({
13 | 'users': users.filter(Boolean)
14 | });
15 | });
16 |
17 | usersRouter.post('/', function(req, res) {
18 | var user = req.body.user;
19 | user.id = users.length + 1;
20 | users.push(user);
21 |
22 | res.status(201);
23 | res.send({
24 | 'users': user
25 | });
26 | });
27 |
28 | usersRouter.get('/:id', function(req, res) {
29 | res.send({
30 | 'users': users[+req.params.id - 1]
31 | });
32 | });
33 |
34 | usersRouter.put('/:id', function(req, res) {
35 | var id = req.params.id;
36 | var user = req.body.user;
37 | user.id = id;
38 | users[+id - 1] = user;
39 |
40 | res.send({
41 | 'users': {
42 | id: req.params.id
43 | }
44 | });
45 | });
46 |
47 | usersRouter.delete('/:id', function(req, res) {
48 | delete users[+req.params.id - 1];
49 | res.status(204).end();
50 | });
51 |
52 | app.use('/users', usersRouter);
53 | };
54 |
--------------------------------------------------------------------------------
/tests/acceptance/user-reload-record-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Reload Record', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-reload-id-1';
27 | var userPromise;
28 |
29 | Ember.run(function() {
30 | userPromise = store.createRecord('user', {name: name});
31 | });
32 |
33 | userPromise.save().then(function(user) {
34 | // #offline
35 | setOnlineStatus(false);
36 | return user.reload();
37 |
38 | }).then(function(user) {
39 | expect(user.get('name')).to.equal(name);
40 | setOnlineStatus(true);
41 | return user.destroyRecord();
42 |
43 | }).then(function() {
44 | // NOTE: wait until saveLocal is finished,
45 | // otherwise we would get strange error when destroying the app.
46 | andLater(function() {
47 | done();
48 | });
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tests/acceptance/user-fetch-by-id-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Fetch By Id', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-fetch-by-id-1';
27 | var userPromise;
28 |
29 | Ember.run(function() {
30 | userPromise = store.createRecord('user', {name: name});
31 | });
32 |
33 | userPromise.save().then(function(userCreated) {
34 | // #offline
35 | setOnlineStatus(false);
36 | return store.fetchById('user', userCreated.get('id'));
37 |
38 | }).then(function(userFetched) {
39 | expect(userFetched.get('name')).to.equal(name);
40 | setOnlineStatus(true);
41 | return userFetched.destroyRecord();
42 |
43 | }).then(function() {
44 | // NOTE: wait until saveLocal is finished,
45 | // otherwise we would get strange error when destroying the app.
46 | andLater(function() {
47 | done();
48 | });
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/addon/stores/local-store.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 | import LFAdapter from 'ember-localforage-adapter/adapters/localforage';
3 | import LFSerializer from 'ember-localforage-adapter/serializers/localforage';
4 | import generateUniqueId from '../utils/generate-unique-id';
5 |
6 | /**
7 | *
8 | * @class FryctoriaLocalStore
9 | * @extends DS.Store
10 | */
11 | export default DS.Store.extend({
12 | init: function() {
13 | var container = this.get('container');
14 |
15 | var serializer = LFSerializer.create({ container: container });
16 | var adapter = LFAdapter
17 | .extend({generateIdForRecord: generateUniqueId})
18 | .create({
19 | container: container,
20 | serializer: serializer,
21 | clear: clearLFAdapter
22 | });
23 |
24 | this.set('adapter', adapter);
25 |
26 | this._super.apply(this, arguments);
27 | },
28 |
29 | /**
30 | * Serializer is fetched via this method or adapter.serializer
31 | *
32 | * @method serializerFor
33 | * @public
34 | */
35 | serializerFor: function() {
36 | return this.get('adapter.serializer');
37 | },
38 |
39 | adapterFor: function() {
40 | return this.get('adapter');
41 | },
42 | });
43 |
44 | function clearLFAdapter() {
45 | // clear cache
46 | var cache = this.get('cache');
47 | if(cache) {
48 | cache.clear();
49 | }
50 |
51 | // clear data in localforage
52 | return window.localforage.setItem('DS.LFAdapter', []);
53 | }
54 |
--------------------------------------------------------------------------------
/tests/acceptance/user-find-by-id-private-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Find By Id(Private)', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-find-by-id-1';
27 | var userPromise;
28 |
29 | Ember.run(function() {
30 | userPromise = store.createRecord('user', {name: name}).save();
31 | });
32 |
33 | userPromise.then(function(userCreated) {
34 | // #offline
35 | setOnlineStatus(false);
36 | userCreated.unloadRecord();
37 | return store.findById('user', userCreated.get('id'));
38 |
39 | }).then(function(userFetched) {
40 | expect(userFetched.get('name')).to.equal(name);
41 | setOnlineStatus(true);
42 | return userFetched.destroyRecord();
43 |
44 | }).then(function() {
45 | // NOTE: wait until saveLocal is finished,
46 | // otherwise we would get strange error when destroying the app.
47 | andLater(function() {
48 | done();
49 | });
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/tests/acceptance/syncer-reset-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 | var RSVP = Ember.RSVP;
14 |
15 | describe('Acceptance: Syncer Reset', function() {
16 | beforeEach(function() {
17 | App = startApp();
18 | store = App.__container__.lookup('store:main');
19 | });
20 |
21 | afterEach(function() {
22 | setOnlineStatus(true);
23 | Ember.run(App, 'destroy');
24 | });
25 |
26 | it('works when offline', function(done) {
27 | var name = 'syncer-reset-user-id-1';
28 | var usersPromise;
29 |
30 | // #offline
31 | setOnlineStatus(false);
32 | Ember.run(function() {
33 | usersPromise = [
34 | store.createRecord('user', {name: name}).save(),
35 | ];
36 | });
37 |
38 | RSVP.all(usersPromise).then(function(users) {
39 | return store.fetchAll('user');
40 |
41 | }).then(function(users) {
42 | var user = users.findBy('name', name);
43 | expect(user).to.exist();
44 |
45 | store.unloadAll('user');
46 | return store.get('syncer').reset();
47 |
48 | }).then(function() {
49 | return store.fetchAll('user');
50 |
51 | }).then(function(users) {
52 | expect(users.get('length')).to.equal(0);
53 | expect(store.get('syncer.jobs.length')).to.equal(0);
54 | done();
55 | });
56 | });
57 | });
58 |
59 |
--------------------------------------------------------------------------------
/tests/acceptance/user-create-delete-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Create and Delete', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-create-delete-1';
27 | var userPromise;
28 |
29 | // #offline
30 | setOnlineStatus(false);
31 |
32 | Ember.run(function() {
33 | userPromise = store.createRecord('user', {name: name});
34 | });
35 |
36 | userPromise.save().then(function(user) {
37 | return user.destroyRecord();
38 |
39 | }).then(function() {
40 | // #online
41 | setOnlineStatus(true);
42 | return store.find('user');
43 |
44 | }).then(function(users) {
45 | // assertions
46 | var userDeleted = users.findBy('name', name);
47 | expect(userDeleted).not.to.exist();
48 |
49 | }).then(function() {
50 | // NOTE: wait until saveLocal is finished,
51 | // otherwise we would get strange error when destroying the app.
52 | andLater(function() {
53 | done();
54 | });
55 | });
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-fryctoria",
3 | "version": "1.0.0-alpha.2",
4 | "description": "Make ember data work offline with automatic sync.",
5 | "directories": {
6 | "doc": "doc",
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "start": "ember server",
11 | "build": "ember build",
12 | "test": "ember test"
13 | },
14 | "repository": "https://github.com/poetic/ember-fryctoria",
15 | "engines": {
16 | "node": ">= 0.10.0"
17 | },
18 | "author": "Chun Yang",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "body-parser": "^1.12.0",
22 | "broccoli-asset-rev": "^2.0.0",
23 | "connect-restreamer": "^1.0.1",
24 | "ember-cli": "0.2.0",
25 | "ember-cli-app-version": "0.3.2",
26 | "ember-cli-babel": "^4.0.0",
27 | "ember-cli-content-security-policy": "0.3.0",
28 | "ember-cli-dependency-checker": "0.0.8",
29 | "ember-cli-htmlbars": "0.7.4",
30 | "ember-cli-ic-ajax": "0.1.1",
31 | "ember-cli-inject-live-reload": "^1.3.0",
32 | "ember-cli-mocha": "^0.5.0",
33 | "ember-cli-uglify": "1.0.1",
34 | "ember-data": "1.0.0-beta.16.1",
35 | "ember-export-application-global": "^1.0.2",
36 | "ember-localforage-adapter": "0.6.9",
37 | "express": "^4.8.5",
38 | "glob": "^4.0.5",
39 | "morgan": "^1.5.2"
40 | },
41 | "keywords": [
42 | "ember-addon",
43 | "sync",
44 | "offline",
45 | "ember-data",
46 | "ember-data store"
47 | ],
48 | "ember-addon": {
49 | "configPath": "tests/dummy/config",
50 | "defaultBlueprint": "application-store"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/addon/stores/main-store/decorate-api-call.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | /*
4 | * syncUp before finder
5 | * syncDown after finder
6 | * @param {String} finderType {'single'|'multiple'|'all'}
7 | */
8 | export default function decorateAPICall(finderType) {
9 | return function apiCall() {
10 | var store = this;
11 | var args = arguments;
12 | var syncer = store.get('syncer');
13 | var _superFinder = store.__nextSuper;
14 |
15 | if(args.length > 0) {
16 | var options = args[args.length - 1];
17 | var bypass = (typeof options === 'object') && options.bypass;
18 | if(bypass) {
19 | return _superFinder.apply(store, args);
20 | }
21 | }
22 |
23 | return syncUp()
24 | .then(function() { return _superFinder.apply(store, args); })
25 | .then(syncDown);
26 |
27 | function syncUp() {
28 | return syncer.syncUp().catch(function(error) {
29 | Ember.Logger.warn('Syncing Error:');
30 | Ember.Logger.error(error && error.stack);
31 | });
32 | }
33 |
34 | function syncDown(result) {
35 | if(finderType === 'all') {
36 | var typeName = result.get('type.typeKey');
37 | syncer.syncDown(typeName);
38 |
39 | } else if(finderType === 'single'){
40 | syncer.syncDown(result);
41 |
42 | } else if(finderType === 'multiple'){
43 | syncer.syncDown(result);
44 |
45 | } else {
46 | throw new Error('finderType must be one of single, multiple or all, but got ' + finderType);
47 | }
48 |
49 | return result;
50 | }
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/addon/stores/main-store/decorate-serializer.js:
--------------------------------------------------------------------------------
1 | import isObject from '../../utils/is-object';
2 |
3 | /*
4 | * Extend serializer so that we can use local serializer when local adater is
5 | * userd.
6 | */
7 | export default function decorateSerializer(serializer, container) {
8 | if(serializer.get('fryctoria')) {
9 | return serializer;
10 | }
11 |
12 | serializer.set('fryctoria', true);
13 |
14 | var localSerializer = container.lookup('store:local').get('adapter.serializer');
15 |
16 | // serialize()
17 | // extract()
18 | // normalize() is not used in localforage adapter, so we do not decorate
19 | decorateSerializerMethod(serializer, localSerializer, 'serialize', 0);
20 | decorateSerializerMethod(serializer, localSerializer, 'extract', 2);
21 | // decorateSerializerMethod(serializer, localSerializer, 'normalize', 2);
22 |
23 | return serializer;
24 | }
25 |
26 | function decorateSerializerMethod(serializer, localSerializer, methodName, wrappedArgIndex) {
27 | var originMethod = serializer[methodName];
28 | var backupMethod = function() {
29 | // remove fryctoria from arg
30 | var args = Array.prototype.slice.call(arguments);
31 | delete args[wrappedArgIndex].fryctoria;
32 |
33 | return localSerializer[methodName].apply(localSerializer, args);
34 | };
35 |
36 | serializer[methodName] = function() {
37 | var payload = arguments[wrappedArgIndex];
38 |
39 | if(isObject(payload) && payload.fryctoria) {
40 | return backupMethod.apply(localSerializer, arguments);
41 | } else {
42 | return originMethod.apply(serializer, arguments);
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/tests/acceptance/user-create-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Create ', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-create-1';
27 | var age = 98;
28 | var userPromise;
29 |
30 | // #offline
31 | setOnlineStatus(false);
32 |
33 | Ember.run(function() {
34 | userPromise = store.createRecord('user', {name: name, age: age});
35 | });
36 |
37 | userPromise.save().then(function() {
38 | // #online
39 | setOnlineStatus(true);
40 | return store.find('user');
41 | }).then(function(users) {
42 | var userCreated = users.findBy('name', name);
43 | expect(userCreated).to.exist('Record should be created and returned from server');
44 | expect(userCreated.get('id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
45 | return userCreated;
46 | }).then(function(userCreated) {
47 | // cleanup
48 | return userCreated.destroyRecord();
49 |
50 | }).then(function() {
51 | // NOTE: wait until saveLocal is finished,
52 | // otherwise we would get strange error when destroying the app.
53 | andLater(function() {
54 | done();
55 | });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/tests/acceptance/animal-create-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: Animal Create ', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var nickName = 'animal-create-1';
27 | var animalPromise;
28 |
29 | // #offline
30 | setOnlineStatus(false);
31 |
32 | Ember.run(function() {
33 | animalPromise = store.createRecord('animal', {nickName: nickName});
34 | });
35 |
36 | animalPromise.save().then(function() {
37 | // #online
38 | setOnlineStatus(true);
39 | return store.find('animal');
40 |
41 | }).then(function(animals) {
42 | var animalCreated = animals.findBy('nickName', nickName);
43 | expect(animalCreated).to.exist('Record should be created and returned from server');
44 | expect(animalCreated.get('id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
45 | return animalCreated;
46 |
47 | }).then(function(animalCreated) {
48 | // cleanup
49 | return animalCreated.destroyRecord();
50 |
51 | }).then(function() {
52 | // NOTE: wait until saveLocal is finished,
53 | // otherwise we would get strange error when destroying the app.
54 | andLater(function() {
55 | done();
56 | });
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/tests/acceptance/user-fetch-all-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 | var RSVP = Ember.RSVP;
14 |
15 | describe('Acceptance: User Fetch All', function() {
16 | beforeEach(function() {
17 | App = startApp();
18 | store = App.__container__.lookup('store:main');
19 | });
20 |
21 | afterEach(function() {
22 | setOnlineStatus(true);
23 | Ember.run(App, 'destroy');
24 | });
25 |
26 | it('works when offline', function(done) {
27 | var name1 = 'user-fetch-all-id-1';
28 | var name2 = 'user-fetch-all-id-2';
29 | var usersPromise;
30 |
31 | Ember.run(function() {
32 | usersPromise = [
33 | store.createRecord('user', {name: name1}).save(),
34 | store.createRecord('user', {name: name2}).save()
35 | ];
36 | });
37 |
38 | RSVP.all(usersPromise).then(function(users) {
39 | // #offline
40 | setOnlineStatus(false);
41 | return store.fetchAll('user');
42 |
43 | }).then(function(users) {
44 | var user1 = users.findBy('name', name1);
45 | var user2 = users.findBy('name', name2);
46 | expect(user1.get('name')).to.equal(name1);
47 | expect(user2.get('name')).to.equal(name2);
48 |
49 | setOnlineStatus(true);
50 | return RSVP.all([
51 | user1.destroyRecord(),
52 | user2.destroyRecord(),
53 | ]);
54 |
55 | }).then(function() {
56 | // NOTE: wait until saveLocal is finished,
57 | // otherwise we would get strange error when destroying the app.
58 | andLater(function() {
59 | done();
60 | });
61 | });
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/addon/utils/reload-local-records.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | var RSVP = Ember.RSVP;
4 |
5 | /*
6 | * This method does not change store, only change localforage
7 | *
8 | * @method reloadLocalRecords
9 | * @param {DS.Store} store store:main
10 | * @param {String|DS.Model} type
11 | */
12 | export default function reloadLocalRecords(container, type) {
13 | var store = container.lookup('store:main');
14 | var modelType = store.modelFor(type);
15 |
16 | var localStore = container.lookup('store:local');
17 | var localAdapter = localStore.get('adapter');
18 |
19 | var reloadedRecords = localAdapter.findAll(localStore, modelType)
20 | .then(deleteAll)
21 | .then(createAll);
22 |
23 | return reloadedRecords;
24 |
25 | function deleteAll(localRecords) {
26 | var deletedRecords = localRecords.map(function(record) {
27 | return localAdapter.deleteRecord(localStore, modelType, {id: record.id});
28 | });
29 |
30 | return RSVP.all(deletedRecords);
31 | }
32 |
33 | function createAll() {
34 | var records = store.all(type);
35 | var createdRecords = records.map(function(record) {
36 | return createLocalRecord(localAdapter, localStore, modelType, record);
37 | });
38 |
39 | return RSVP.all(createdRecords);
40 | }
41 | }
42 |
43 | function createLocalRecord(localAdapter, localStore, modelType, record) {
44 | if(record.get('id')) {
45 | var snapshot = record._createSnapshot();
46 | return localAdapter.createRecord(localStore, modelType, snapshot);
47 |
48 | } else {
49 | var recordName = record.constructor && record.constructor.typeKey;
50 | var warnMessage = 'Record ' + recordName + ' does not have an id, therefor we can not create it locally: ';
51 |
52 | var recordData = record.toJSON && record.toJSON();
53 |
54 | Ember.Logger.warn(warnMessage, recordData);
55 |
56 | return RSVP.resolve();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/acceptance/user-delete-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 | var RSVP = Ember.RSVP;
14 |
15 | describe('Acceptance: User Delete', function() {
16 | beforeEach(function() {
17 | App = startApp();
18 | store = App.__container__.lookup('store:main');
19 | });
20 |
21 | afterEach(function() {
22 | setOnlineStatus(true);
23 | Ember.run(App, 'destroy');
24 | });
25 |
26 | it('works when offline', function(done) {
27 | this.timeout(10000);
28 |
29 | var name = 'user-delete-1';
30 | var age = 98;
31 | var userPromise;
32 |
33 | // NOTE: delete all user with the same name, make tests more robust
34 | store.find('user').then(function(users) {
35 | return RSVP.all(users.map(function(user) {
36 | if(user.get('name') === name) {
37 | return user.destroyRecord();
38 | }
39 | }));
40 |
41 | }).then(function() {
42 | Ember.run(function() {
43 | userPromise = store.createRecord('user', {name: name, age: age});
44 | });
45 | return userPromise.save();
46 |
47 | }).then(function(user) {
48 | // #offline delete
49 | setOnlineStatus(false);
50 | return user.destroyRecord();
51 |
52 | }).then(function() {
53 | // #online
54 | setOnlineStatus(true);
55 | return store.find('user');
56 |
57 | }).then(function(users) {
58 | // assertions
59 | var userDeleted = users.findBy('name', name);
60 | expect(userDeleted).not.to.exist();
61 |
62 | }).then(function() {
63 | // NOTE: wait until reloadLocalRecords is finished,
64 | // otherwise we would get strange error when destroying the app.
65 | andLater(function() {
66 | done();
67 | }, 1000);
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/tests/acceptance/user-update-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Update', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-create-1';
27 | var nameUpdated = 'user-create-2';
28 | var age = 98;
29 | var ageUpdated = 102;
30 | var userPromise;
31 |
32 | Ember.run(function() {
33 | userPromise = store.createRecord('user', {name: name, age: age});
34 | });
35 |
36 | userPromise.save().then(function(user) {
37 | // #offline update
38 | setOnlineStatus(false);
39 | return user.setProperties({name: nameUpdated, age: ageUpdated}).save();
40 |
41 | }).then(function() {
42 | // #online
43 | setOnlineStatus(true);
44 | return store.find('user');
45 |
46 | }).then(function(users) {
47 | // assertions
48 | var userUpdated = users.findBy('name', nameUpdated);
49 | expect(userUpdated).to.exist('Record should be created and returned from server');
50 | expect(userUpdated.get('id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
51 | expect(userUpdated.get('name')).to.equal(nameUpdated);
52 | expect(userUpdated.get('age')).to.equal(ageUpdated);
53 | return userUpdated;
54 |
55 | }).then(function(userUpdated) {
56 | // cleanup
57 | return userUpdated.destroyRecord();
58 |
59 | }).then(function() {
60 | // NOTE: wait until saveLocal is finished,
61 | // otherwise we would get strange error when destroying the app.
62 | andLater(function() {
63 | done();
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/tests/acceptance/user-create-update-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 |
14 | describe('Acceptance: User Create and Update', function() {
15 | beforeEach(function() {
16 | App = startApp();
17 | store = App.__container__.lookup('store:main');
18 | });
19 |
20 | afterEach(function() {
21 | setOnlineStatus(true);
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | var name = 'user-create-update-1';
27 | var nameUpdated = 'user-create-update-2';
28 | var age = 98;
29 | var ageUpdated = 102;
30 | var userPromise;
31 |
32 | Ember.run(function() {
33 | // #offline
34 | setOnlineStatus(false);
35 | userPromise = store.createRecord('user', {name: name, age: age});
36 | });
37 |
38 | userPromise.save().then(function(user) {
39 | return user.setProperties({name: nameUpdated, age: ageUpdated}).save();
40 |
41 | }).then(function() {
42 | // #online
43 | setOnlineStatus(true);
44 | return store.find('user');
45 |
46 | }).then(function(users) {
47 | // assertions
48 | var userUpdated = users.findBy('name', nameUpdated);
49 | expect(userUpdated).to.exist('Record should be created and returned from server');
50 | expect(userUpdated.get('id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
51 | expect(userUpdated.get('name')).to.equal(nameUpdated);
52 | expect(userUpdated.get('age')).to.equal(ageUpdated);
53 | return userUpdated;
54 | }).then(function(userUpdated) {
55 | // cleanup
56 | return userUpdated.destroyRecord();
57 |
58 | }).then(function() {
59 | // NOTE: wait until saveLocal is finished,
60 | // otherwise we would get strange error when destroying the app.
61 | andLater(function() {
62 | done();
63 | });
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/addon/stores/main-store.js:
--------------------------------------------------------------------------------
1 | import DS from 'ember-data';
2 | import decorateAdapter from './main-store/decorate-adapter';
3 | import decorateSerializer from './main-store/decorate-serializer';
4 | import decorateAPICall from './main-store/decorate-api-call';
5 |
6 | /**
7 | * This will be used as store:main
8 | *
9 | * @class FryctoriaMainStore
10 | * @extends DS.Store
11 | */
12 | export default DS.Store.extend({
13 | /**
14 | This method returns a fresh collection from the server, regardless of if there is already records
15 | in the store or not.
16 | @method fetchAll
17 | @param {String or subclass of DS.Model} type
18 | @return {Promise} promise
19 | */
20 | fetchAll: decorateAPICall('all'),
21 |
22 | /*
23 | * fetchById use the following methods:
24 | * find -> findById
25 | * model#reload -> store#reloadRecord
26 | * NOTE: this will trigger syncUp twice, this is OK. And since this is
27 | * a public method, we probably want to preserve this.
28 | */
29 | fetchById: decorateAPICall('single'),
30 |
31 | /**
32 | This method returns a record for a given type and id combination.
33 | It is Used by:
34 | #find(<- #fetchById)
35 | #findByIds(private, orphan)
36 | @method findById
37 | @private
38 | @param {String or subclass of DS.Model} type
39 | @param {String|Integer} id
40 | @param {Object} preload - optional set of attributes and relationships passed in either as IDs or as actual models
41 | @return {Promise} promise
42 | */
43 | findById: decorateAPICall('single'),
44 |
45 | /**
46 | * Used by:
47 | * #find
48 | * model#reload
49 | */
50 |
51 | /**
52 | This method is called by the record's `reload` method.
53 | This method calls the adapter's `find` method, which returns a promise. When
54 | **that** promise resolves, `reloadRecord` will resolve the promise returned
55 | by the record's `reload`.
56 | @method reloadRecord
57 | @private
58 | @param {DS.Model} record
59 | @return {Promise} promise
60 | */
61 | reloadRecord: decorateAPICall('single'),
62 |
63 | adapterFor: function(type) {
64 | var adapter = this._super(type);
65 | return decorateAdapter(adapter, this.container);
66 | },
67 |
68 | serializerFor: function(type) {
69 | var serializer = this._super(type);
70 | return decorateSerializer(serializer, this.container);
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/tests/acceptance/team-create-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 | var RSVP = Ember.RSVP;
14 |
15 | describe('Acceptance: Create A Team(hasMany users)', function() {
16 | beforeEach(function() {
17 | App = startApp();
18 | store = App.__container__.lookup('store:main');
19 | });
20 |
21 | afterEach(function() {
22 | Ember.run(App, 'destroy');
23 | });
24 |
25 | it('works when offline', function(done) {
26 | this.timeout(10000);
27 |
28 | var users, userRecords;
29 |
30 | Ember.run(function() {
31 | users = [
32 | store.createRecord('user', { name: 'create-team-user-1' }).save(),
33 | store.createRecord('user', { name: 'create-team-user-2' }).save()
34 | ];
35 | });
36 |
37 | var teamName = 'create-team-team-1';
38 |
39 | // #online
40 | RSVP.all(users).then(function(users) {
41 | userRecords = users;
42 |
43 | // #offline
44 | setOnlineStatus(false);
45 | // create a team with hasMany relationship while offline:
46 | var team = store.createRecord('team', { name: teamName });
47 | team.get('users').pushObjects(users);
48 |
49 | return team.save();
50 | }).then(function(team) {
51 | // #online
52 | setOnlineStatus(true);
53 | // after online, we do the syncing by geting all teams:
54 | return store.find('team');
55 | }).then(function(teams) {
56 | var team = teams.findBy('name', teamName);
57 | expect(team).to.exist('Team should be created and returned from server');
58 | expect(team.get('id')).not.to.include('fryctoria', 'Team should have a remoteId instead of a generated one');
59 | return team;
60 | }).then(function(team) {
61 | // clean up, remove teams and users created
62 | var deletedUsers = userRecords.map(function(user) {
63 | return user.destroyRecord();
64 | });
65 | return RSVP.all(deletedUsers).then(function() {
66 | return team.destroyRecord();
67 | });
68 | }).then(function() {
69 | // NOTE: wait until reloadLocalRecords is finished,
70 | // otherwise we would get strange error when destroying the app.
71 | andLater(function() {
72 | done();
73 | }, 1000);
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/tests/acceptance/job-create-test.js:
--------------------------------------------------------------------------------
1 | /* jshint expr:true */
2 | import {
3 | describe,
4 | it,
5 | beforeEach,
6 | afterEach
7 | } from 'mocha';
8 | import { expect } from 'chai';
9 | import Ember from 'ember';
10 | import startApp from '../helpers/start-app';
11 |
12 | var App, store;
13 | var RSVP = Ember.RSVP;
14 |
15 | describe('Acceptance: Job(belongsTo) Create', function() {
16 | beforeEach(function() {
17 | App = startApp();
18 | store = App.__container__.lookup('store:main');
19 | });
20 |
21 | afterEach(function() {
22 | setOnlineStatus(true);
23 | Ember.run(App, 'destroy');
24 | });
25 |
26 | it('works when offline', function(done) {
27 | this.timeout(10000);
28 |
29 | var userName = 'job-create-user-1';
30 | var jobName = 'job-create-1';
31 | var userPromise;
32 | var jobPromise;
33 |
34 | // #offline
35 | setOnlineStatus(false);
36 |
37 | Ember.run(function() {
38 | userPromise = store.createRecord('user', {name: userName});
39 | });
40 |
41 | userPromise.save().then(function(user) {
42 | jobPromise = store.createRecord('job', {name: jobName, user: user}).save();
43 | return jobPromise;
44 |
45 | }).then(function(job) {
46 | setOnlineStatus(true);
47 | return store.find('job');
48 |
49 | }).then(function(jobs) {
50 | var jobCreated = jobs.findBy('name', jobName);
51 | expect(jobCreated).to.exist('Record should be created and returned from server');
52 | expect(jobCreated.get('id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
53 | expect(jobCreated.get('user.id')).to.exist();
54 | expect(jobCreated.get('user.id')).not.to.include('fryctoria', 'Record should have a remoteId instead of a generated one');
55 | return jobCreated;
56 | }).then(function(job) {
57 | var user = job.get('user');
58 | return RSVP.all([job.destroyRecord(), user.destroyRecord()]);
59 |
60 | }).then(function() {
61 | // wait untill job and user are deleted locally
62 | andLater(function() {
63 | setOnlineStatus(false);
64 | return RSVP.all([store.find('user'), store.find('job')]).then(function(data) {
65 | var user = data[0].findBy('name', userName);
66 | var job = data[1].findBy('name', jobName);
67 | expect(user).not.to.exist();
68 | expect(job).not.to.exist();
69 | });
70 | }, 1000);
71 |
72 | }).then(function() {
73 | // NOTE: wait until saveLocal is finished,
74 | // otherwise we would get strange error when destroying the app.
75 | andLater(function() {
76 | done();
77 | }, 1000);
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/addon/stores/main-store/decorate-adapter.js:
--------------------------------------------------------------------------------
1 | import backup from '../../utils/backup';
2 | import isObject from '../../utils/is-object';
3 | import generateUniqueId from '../../utils/generate-unique-id';
4 |
5 | /*
6 | * Extend adapter so that we can use local adapter when offline
7 | */
8 | export default function decorateAdapter(adapter, container) {
9 | if(adapter.get('fryctoria')) {
10 | return adapter;
11 | }
12 |
13 | adapter.set('fryctoria', {});
14 |
15 | var localAdapter = container.lookup('store:local').get('adapter');
16 |
17 | // find()
18 | // findAll()
19 | // findQuery()
20 | // findMany()
21 | // createRecord()
22 | // updateRecord()
23 | // deleteRecord()
24 | var methods = [
25 | 'find', 'findAll', 'findQuery', 'findMany',
26 | 'createRecord', 'updateRecord', 'deleteRecord'
27 | ];
28 |
29 | methods.forEach(function(methodName) {
30 | decorateAdapterMethod(adapter, localAdapter, methodName);
31 | });
32 |
33 | return adapter;
34 | }
35 |
36 | function decorateAdapterMethod(adapter, localAdapter, methodName) {
37 | var originMethod = adapter[methodName];
38 | var backupMethod = createBackupMethod(localAdapter, methodName);
39 | var isOffline = adapter.container.lookup('syncer:main').isOffline;
40 |
41 | adapter[methodName] = function() {
42 | return originMethod.apply(adapter, arguments)
43 | .catch(backup(isOffline, backupMethod, arguments));
44 | };
45 |
46 | adapter.fryctoria[methodName] = originMethod;
47 | }
48 |
49 | function createBackupMethod(localAdapter, methodName) {
50 | var container = localAdapter.container;
51 | var crudMethods = ['createRecord', 'updateRecord', 'deleteRecord'];
52 | var isCRUD = crudMethods.indexOf(methodName) !== -1;
53 | var isCreate = methodName === 'createRecord';
54 |
55 | return function backupMethod() {
56 | var args = Array.prototype.slice.call(arguments);
57 |
58 | // ---------- CRUD specific
59 | if(isCRUD) {
60 | var snapshot = args[2];
61 |
62 | if(isCreate) {
63 | snapshot = addIdToSnapshot(snapshot);
64 | }
65 |
66 | createJobInSyncer(container, methodName, snapshot);
67 |
68 | // decorate snapshot for serializer#serialize, this should be after
69 | // createJob in syncer
70 | snapshot.fryctoria = true;
71 | args[2] = snapshot;
72 | }
73 | // ---------- CRUD specific END
74 |
75 | return localAdapter[methodName].apply(localAdapter, args)
76 | .then(function(payload) {
77 | // decorate payload for serializer#extract
78 | if(isObject(payload)) {
79 | payload.fryctoria = true;
80 | }
81 | return payload;
82 | });
83 | };
84 | }
85 |
86 | // Add an id to record before create in local
87 | function addIdToSnapshot(snapshot) {
88 | var record = snapshot.record;
89 | record.get('store').updateId(record, {id: generateUniqueId()});
90 | return record._createSnapshot();
91 | }
92 |
93 | function createJobInSyncer(container, methodName, snapshot) {
94 | var syncer = container.lookup('syncer:main');
95 | syncer.createJob(methodName, snapshot);
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Warning
2 | Now the addon is in alpha, please help by reporting bugs and opening issues.
3 |
4 | # Instruction
5 | ### Step1: Install the addons, generate an initializer
6 | ```bash
7 | ember install ember-localforage-adapter
8 | ember install ember-fryctoria
9 | ember g reopen-syncer-initializer
10 | ```
11 | ### Step2: Offline!
12 |
13 | # Requirements
14 | ember-data v1.0.0-beta.16.1
15 |
16 | # How does it work?
17 | ember-fryctoria utilizes [ember-localforage-adapter](https://github.com/genkgo/ember-localforage-adapter/) to read and write locally.
18 |
19 | It always try to connect to the server and fall back to local data when an offline error is caught.
20 |
21 | When online, it will use your defalut store, adapter and serializer. After each request to server, it will automatically save records into localforage-adapter.
22 |
23 | When offline, it will use the local backup(localforage) to retrive records. A queue of jobs is created when the user create, update or delete while offline. When online we flush this queue to keep the server in sync.
24 |
25 | **Features NOT supported(yet):**
26 | - Sideloaded records are not saved to localforage automatically, only the main
27 | records are saved. [Here](https://github.com/poetic/ember-fryctoria/issues/2) is a work around.
28 | - Changes in embeded records will not be pushed to server if you create or update offline
29 | and try to sync when online. Only the main record will be updated or created.
30 | - Customized transforms are not supported, see work around [here](https://github.com/poetic/ember-fryctoria/issues/1).
31 |
32 |
33 | # How to sync?
34 | An object called *syncer* is responsible for syncing.
35 | - It is registered into *container*, therefore you can get syncer like this
36 | ```javascript
37 | container.lookup('main:syncer')
38 | ```
39 |
40 | - It is also injected into main *store*, therefore you can get syncer like this
41 | ```javascript
42 | store.get('syncer')
43 | ```
44 |
45 | It has a *jobs* property whichs is a queue of operations including create, update and delete. These are your offline operations.
46 |
47 | There are two important methods in *syncer*:
48 |
49 | - syncUp: push local changes to remote server, flush the queue of jobs.
50 | - syncDown: save data in main *store* into localforage.
51 | ```javascript
52 | syncer.syncDown('user'); // remove all records in localforage and save all current user records into localforage
53 | syncer.syncDown(user); // create or update user record into localforage
54 | syncer.syncDown([user1, user2]); // create or update user records into localforage
55 | ```
56 |
57 | ### Automatic sync
58 | In most cases, you do not need to manully sync since ember-fryctoria automatially
59 | *syncUp* before every request to server and automatially
60 | *syncDown* after every request to server.
61 | ```javascript
62 | store.find('user') // syncDown('user') is called.
63 | store.fetchAll('user') // syncDown('user') is called.
64 | store.find('user', 1) // syncDown(user) is called.
65 | store.fetchById('user', 1) // syncDown(user) is called.
66 | user.reload() // syncDown(user) is called.
67 | ```
68 |
69 | ### Manual sync
70 | When you sideload or embed records, you probably want to manully save sideloaded or embeded records to localforage. Also you may want to syncUp periodially. In these cases, you can manully syncDown or syncUp.
71 |
72 |
73 | # How to handle errors during syncUp?
74 | By default, when we get an error during syncUp, syncer will stop syncing. In the
75 | next syncUp, syncer will retry starting from the failed job.
76 |
77 | You can customize this by overwriting *handleSyncUpError* method in syncer in
78 | reopen-syncer-initializer.
79 |
80 | IMO, there is really not a single robust way to handle syncing faliure for
81 | a conventional database like SQL combined with ember data. I would recommand you
82 | to only enable user to read while offline. Or you should implement a robust way
83 | to handle syncing errors for your app.
84 |
85 | # How to decide what is offline?
86 | By default, offline is defined by ```jqXHR && jqXHR.status === 0```.
87 | [jqXHR](http://api.jquery.com/jQuery.ajax/#jqXHR)
88 |
89 | You can overwrite this by overwriting *isOffline* method in syncer in
90 | reopen-syncer-initializer.
91 |
92 | # How to bypass syncing
93 | You can bypass syncing by passing an option object {bypass: true} at the end of
94 | a api call of store:
95 | ```
96 | store.find('user', {bypass: true}) // this would not wait for syncing
97 | ```
98 |
--------------------------------------------------------------------------------
/addon/syncer.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import generateUniqueId from './utils/generate-unique-id';
3 | import reloadLocalRecords from './utils/reload-local-records';
4 | import isModelInstance from './utils/is-model-instance';
5 |
6 | var RSVP = Ember.RSVP;
7 |
8 | /**
9 | We save offline jobs to localforage and run them one at a time when online
10 |
11 | Job schema:
12 | ```
13 | {
14 | id: { String },
15 | operation: { 'createRecord'|'updateRecord'|'deleteRecord' },
16 | typeName: { String },
17 | record: { Object },
18 | createdAt: { Date },
19 | }
20 | ```
21 |
22 | We save remoteIdRecords to localforage. They are used to lookup remoteIds
23 | from localIds.
24 |
25 | RecordId schema:
26 | ```
27 | {
28 | typeName: { String },
29 | localId: { String },
30 | remoteId: { String }
31 | }
32 | ```
33 |
34 | @class Syncer
35 | @extends Ember.Object
36 | */
37 | export default Ember.Object.extend({
38 | db: null,
39 | // initialize jobs since jobs may be used before we fetch from localforage
40 | jobs: [],
41 | remoteIdRecords: [],
42 |
43 | /**
44 | * Initialize db.
45 | *
46 | * Initialize jobs, remoteIdRecords.
47 | *
48 | * @method init
49 | * @private
50 | */
51 | init: function() {
52 | var syncer = this;
53 |
54 | var localStore = syncer.get('container').lookup('store:local');
55 | var localAdapter = localStore.get('adapter');
56 |
57 | syncer.set('db', window.localforage);
58 | syncer.set('localStore', localStore);
59 | syncer.set('localAdapter', localAdapter);
60 |
61 | // NOTE: get remoteIdRecords first then get jobs,
62 | // since jobs depend on remoteIdRecords
63 | syncer.getAll('remoteIdRecord').then(
64 | syncer.getAll.bind(syncer, 'job')
65 | );
66 | },
67 |
68 | /**
69 | * Save an offline job to localforage, this is used in DS.Model.save
70 | *
71 | * @method createJob
72 | * @public
73 | * @param {String} operation
74 | * @param {DS.Snapshot} snapshot
75 | * @return {Promise} jobs
76 | */
77 | createJob: function(operation, snapshot) {
78 | var typeName = snapshot.typeKey;
79 | var serializer = this.get('mainStore').serializerFor(typeName);
80 | snapshot.fryctoria = true;
81 |
82 | return this.create('job', {
83 | id: generateUniqueId('job'),
84 | operation: operation,
85 | typeName: typeName,
86 | record: serializer.serialize(snapshot, {includeId: true}),
87 | createdAt: (new Date()).getTime(),
88 | });
89 | },
90 |
91 | /**
92 | * Run all the jobs to sync up. If there is an error while syncing, it will
93 | * only log the error instead of return a rejected promise. This is called in
94 | * all the methods in DS.Store and DS.Model that talk to the server.
95 | *
96 | * @method syncUp
97 | * @public
98 | * @return {Promise} this promise will always resolve.
99 | */
100 | syncUp: function() {
101 | return this.runAllJobs();
102 | },
103 |
104 | /**
105 | * TODO:
106 | * Save all records in the store into localforage.
107 | *
108 | * @method syncDown
109 | * @public
110 | * @param {String|DS.Model|Array} typeName, record, records
111 | * @return {Promie}
112 | */
113 | syncDown: function(descriptor) {
114 | var syncer = this;
115 |
116 | if(typeof descriptor === 'string') {
117 | return reloadLocalRecords(syncer.get('container'), descriptor);
118 |
119 | } else if(isModelInstance(descriptor)) {
120 | return syncer.syncDownRecord(descriptor);
121 |
122 | } else if(Ember.isArray(descriptor)) {
123 | var updatedRecords = descriptor.map(function(record) {
124 | return syncer.syncDownRecord(record);
125 | });
126 | return RSVP.all(updatedRecords);
127 |
128 | } else {
129 | throw new Error('Input can only be a string, a DS.Model or an array of DS.Model, but is ' + descriptor);
130 | }
131 | },
132 |
133 | /**
134 | * Reset syncer and localforage records.
135 | * Remove all jobs and remoteIdRecords.
136 | * Remove all records in localforage.
137 | *
138 | * @method
139 | * @public
140 | */
141 | reset: function() {
142 | return RSVP.all([
143 | this.deleteAll('job'),
144 | this.deleteAll('remoteIdRecord'),
145 | this.get('localAdapter').clear()
146 | ]);
147 | },
148 |
149 | /**
150 | * Decide if the error indicates offline
151 | *
152 | * @method
153 | * @public
154 | */
155 | isOffline: function(error) {
156 | return error && error.status === 0;
157 | },
158 |
159 | /**
160 | * This method does not talk to remote store, it only need to get serializer
161 | * from a store.
162 | *
163 | * @method
164 | * @private
165 | */
166 | syncDownRecord: function(record) {
167 | var localStore = this.get('localStore');
168 | var localAdapter = this.get('localAdapter');
169 | var snapshot = record._createSnapshot();
170 |
171 | if(record.get('isDeleted')) {
172 | return localAdapter.deleteRecord(localStore, snapshot.type, snapshot);
173 | } else {
174 | return localAdapter.createRecord(localStore, snapshot.type, snapshot);
175 | }
176 | },
177 |
178 | /**
179 | * Attampt to run all the jobs one by one. Deal with syncing failure.
180 | *
181 | * @method runAllJobs
182 | * @private
183 | * @return {Promise}
184 | */
185 | runAllJobs: function() {
186 | var syncer = this;
187 | var jobs = this.get('jobs');
188 |
189 | if(jobs.length === 0) {
190 | Ember.Logger.info('Syncing jobs are empty.');
191 | return RSVP.resolve();
192 | }
193 |
194 | Ember.Logger.info('Syncing started.');
195 | jobs = jobs.sortBy('createdAt');
196 |
197 | // run jobs one at a time
198 | return jobs.reduce(function(acc, job) {
199 | return acc.then(function() {
200 | return syncer.runJob(job);
201 | });
202 | }, RSVP.resolve())
203 |
204 | .then(function() {
205 | syncer.deleteAll('remoteIdRecord');
206 | Ember.Logger.info('Syncing succeed.');
207 | })
208 |
209 | .catch(function(error) {
210 | if(syncer.isOffline(error)) {
211 | Ember.Logger.info('Can not connect to server, stop syncing');
212 | } else if(syncer.handleSyncUpError){
213 | return syncer.handleSyncUpError(error);
214 | } else {
215 | return RSVP.reject(error);
216 | }
217 | });
218 | },
219 |
220 | runJob: function(job) {
221 | var syncer = this;
222 |
223 | var store = syncer.get('mainStore');
224 |
225 | var typeName = job.typeName;
226 | var type = store.modelFor(typeName);
227 |
228 | var adapter = store.adapterFor(typeName);
229 | var remoteCRUD = adapter.get('fryctoria');
230 |
231 | var record = createRecordFromJob(syncer, job, type);
232 | var snapshot = record._createSnapshot();
233 |
234 | var operation = job.operation;
235 |
236 | var syncedRecord;
237 |
238 | if(operation === 'deleteRecord') {
239 | syncedRecord = remoteCRUD.deleteRecord.call(adapter, store, type, snapshot);
240 |
241 | } else if(operation === 'updateRecord') {
242 | // TODO: make reverse update possible
243 | // for now, we do not accept 'reverse update' i.e. update from the server
244 | // will not be reflected in the store
245 | syncedRecord = remoteCRUD.updateRecord.call(adapter, store, type, snapshot);
246 |
247 | } else if(operation === 'createRecord') {
248 | // TODO: make reverse update possible
249 | // for now, we do not accept 'reverse update' i.e. update from the server
250 | // will not be reflected in the store
251 | var recordIdBeforeCreate = record.get('id');
252 | record.set('id', null);
253 | snapshot = record._createSnapshot();
254 |
255 | // adapter -> store -> syncer(remoteId) -> localforage
256 | syncedRecord = remoteCRUD.createRecord.call(adapter, store, type, snapshot)
257 | .then(updateIdInStore)
258 | .then(createRemoteIdRecord)
259 | .then(refreshLocalRecord);
260 | }
261 |
262 | // delete from db after syncing success
263 | return syncedRecord.then(function() {
264 | return syncer.deleteById('job', job.id);
265 | });
266 |
267 | function updateIdInStore(payload) {
268 | var recordExtracted = store.serializerFor(type).extract(
269 | store, type, payload, record.get('id'), 'single'
270 | );
271 |
272 | var recordInStore = store.getById(typeName, recordIdBeforeCreate);
273 |
274 | // INFO: recordInStore may be null because it may be deleted
275 | if(recordInStore) {
276 | recordInStore.set('id', null);
277 | store.updateId(recordInStore, recordExtracted);
278 | }
279 |
280 | return recordExtracted;
281 | }
282 |
283 | function createRemoteIdRecord(recordExtracted) {
284 | return syncer.create('remoteIdRecord', {
285 | typeName: typeName,
286 | localId: recordIdBeforeCreate,
287 | remoteId: recordExtracted.id
288 | }).then(function() {
289 | return recordExtracted;
290 | });
291 | }
292 |
293 | // This method does not talk to store, only to adapter
294 | function refreshLocalRecord(recordExtracted) {
295 | // NOTE: we should pass snapshot instead of rawRecord to deleteRecord,
296 | // in deleteRecord, we only call snapshot.id, we can just pass the
297 | // rawRecord to it.
298 |
299 | // delete existing record with localId
300 | var localStore = syncer.get('localStore');
301 | var localAdapter = syncer.get('localAdapter');
302 |
303 | return localAdapter.deleteRecord(
304 | localStore, type, {id: recordIdBeforeCreate}
305 | ).then(function() {
306 |
307 | // create new record with remoteId
308 | record.set('id', recordExtracted.id);
309 | var snapshot = record._createSnapshot();
310 | return localAdapter.createRecord(localStore, type, snapshot);
311 | });
312 | }
313 | },
314 |
315 | // lazy evaluate main store, since main store is initialized after syncer
316 | mainStore: Ember.computed(function() {
317 | return this.get('container').lookup('store:main');
318 | }),
319 |
320 | getRemoteId: function(typeName, id) {
321 | Ember.assert('Id can not be blank.', !Ember.isNone(id));
322 |
323 | if(isRemoteId(id)) {
324 | return id;
325 | }
326 |
327 | // Try to find a remote id from local id
328 | // NOTE: it is possible we are trying to create one record in local
329 | // and does not have a remote id yet.
330 | var remoteIdRecord = this.get('remoteIdRecords').find(function(record) {
331 | return record.typeName === typeName && record.localId === id;
332 | });
333 |
334 | return remoteIdRecord ? remoteIdRecord.remoteId : id;
335 | },
336 |
337 | // CRUD for jobs and remoteIdRecords
338 | getAll: function(typeName) {
339 | var syncer = this;
340 | var namespace = getNamespace(typeName);
341 |
342 | return syncer.get('db').getItem(namespace).then(function(records) {
343 | records = records || [];
344 | syncer.set(pluralize(typeName), records);
345 | return records;
346 | });
347 | },
348 |
349 | deleteAll: function(typeName) {
350 | return this.saveAll(typeName, []);
351 | },
352 |
353 | deleteById: function(typeName, id) {
354 | var records = this.get(pluralize(typeName)).filter(function(record) {
355 | return id !== record.id;
356 | });
357 |
358 | return this.saveAll(typeName, records);
359 | },
360 |
361 | create: function(typeName, record) {
362 | var records = this.get(pluralize(typeName));
363 | records.push(record);
364 |
365 | return this.saveAll(typeName, records);
366 | },
367 |
368 | saveAll: function(typeName, records) {
369 | this.set(pluralize(typeName), records);
370 |
371 | var namespace = getNamespace(typeName);
372 | return this.get('db').setItem(namespace, records);
373 | },
374 | });
375 |
376 | function pluralize(typeName) {
377 | return typeName + 's';
378 | }
379 |
380 | function getNamespace(typeName) {
381 | var LocalForageKeyHash = {
382 | 'job': 'EmberFryctoriaJobs',
383 | 'remoteIdRecord': 'EmberFryctoriaRemoteIdRecords',
384 | };
385 | return LocalForageKeyHash[typeName];
386 | }
387 |
388 | function isRemoteId(id) {
389 | return id.indexOf('fryctoria') !== 0;
390 | }
391 |
392 | function createRecordFromJob(syncer, job, type) {
393 | var remoteId = syncer.getRemoteId(job.typeName, job.record.id);
394 | var record = createRecordInLocalStore(syncer, type, remoteId);
395 |
396 | record.setupData(job.record);
397 | record.eachRelationship(function(name, descriptor) {
398 | addRelationshipToRecord(name, descriptor, job.record, record, syncer);
399 | }); // load relationships
400 |
401 | return record;
402 | }
403 |
404 | function addRelationshipToRecord(name, descriptor, jobRecord, record, syncer) {
405 | var relationship = jobRecord[name];
406 | var relationshipId, relationshipIds;
407 | var relationshipTypeName = descriptor.type.typeKey;
408 |
409 | if(!relationship) { return; }
410 |
411 | if(descriptor.kind === 'belongsTo') {
412 | var belongsToRecord;
413 | // belongsTo
414 | relationshipId = relationship;
415 | relationshipId = syncer.getRemoteId(relationshipTypeName, relationshipId);
416 | // NOTE: It is possible that the association is deleted in the store
417 | // and getById is null, so we create a fake record with the right id
418 | belongsToRecord = getOrCreateRecord(syncer, descriptor.type, relationshipId);
419 | record.set(name, belongsToRecord);
420 |
421 | } else if(descriptor.kind === 'hasMany') {
422 | var hasManyRecords;
423 | // hasMany
424 | relationshipIds = relationship || [];
425 | hasManyRecords = relationshipIds.map(function(id) {
426 | var remoteId = syncer.getRemoteId(relationshipTypeName, id);
427 | return getOrCreateRecord(syncer, descriptor.type, remoteId);
428 | });
429 | record.get(descriptor.key).pushObjects(hasManyRecords);
430 | }
431 | }
432 |
433 | function getOrCreateRecord(syncer, type, id) {
434 | var mainStore = syncer.get('mainStore');
435 |
436 | return mainStore.getById(type.typeKey, id) ||
437 | createRecordInLocalStore(syncer, type, id);
438 | }
439 |
440 |
441 | function createRecordInLocalStore(syncer, type, id) {
442 | // after create, the state becomes "root.empty"
443 | var record = type._create({
444 | id: id,
445 | store: syncer.get('localStore'),
446 | container: syncer.get('container'),
447 | });
448 |
449 | // after setupData, the state becomes "root.loaded.saved"
450 | record.setupData({});
451 | return record;
452 | }
453 |
--------------------------------------------------------------------------------