@cityads/ember-data-change-tracker 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,15 @@
1
+ /* global module */
2
+ module.exports = {
3
+ root: true,
4
+ parserOptions: {
5
+ ecmaVersion: 2017,
6
+ sourceType: 'module'
7
+ },
8
+ extends: 'eslint:recommended',
9
+ env: {
10
+ browser: true
11
+ },
12
+ rules: {
13
+ "no-console": "off"
14
+ }
15
+ };
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017
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.
package/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # ember-data-change-tracker
2
+
3
+ [![npm version](https://badge.fury.io/js/@cityads%2Fember-data-change-tracker.svg)](http://badge.fury.io/js/@cityads%2Fember-data-change-tracker)
4
+
5
+ **New**
6
+ - Experimental feature
7
+ - isDirty, hasDirtyRelations computed properties
8
+ - Set up in [configuration](https://github.com/danielspaniel/ember-data-change-tracker#configuration) as { enableIsDirty: true }
9
+ - It is experimental and a has one crippling defect, it can not track object type
10
+ attributes. But if you don't have object types it works fine.
11
+
12
+ This addon aims to fill in the gaps in the change tracking / rollback that ember data does now.
13
+
14
+ - Currently ember-data
15
+ - tracks changes for numbers/strings/date/boolean attributes
16
+ - has a ```changedAttributes()``` method to see what changed => [ last, current ]
17
+ - has a ```rollbackAttributes()``` method to rollback attributes
18
+ - has a ```hasDirtyAttributes``` computed property
19
+
20
+ - This addon:
21
+ - tracks modifications in attributes that are object/json/custom type
22
+ - tracks replacement of belongsTo associations
23
+ - tracks replacement/changes in hasMany associations
24
+ - adds a ```modelChanges()``` method to DS.Model
25
+ - adds a ```rollback()``` method to DS.Model
26
+ - adds a ```isDirty``` computed property to DS.Model ( only if enabled in configuration )
27
+ - adds a ```hasDirtyRelations``` computed property to DS.Model ( only if enabled in configuration )
28
+ - Only works with
29
+ - ember-data versions 2.7+ ( if you have polymphic relationships )
30
+ - ember-data versions 2.5+ ( if you don't )
31
+ - Can be used in two modes
32
+ - auto track mode
33
+ - manual track mode ( the default )
34
+
35
+ ## Installation
36
+
37
+ * `ember install ember-data-change-tracker`
38
+
39
+ ## Why?
40
+
41
+ Say there is a user model like this:
42
+
43
+ ```javascript
44
+ export default Model.extend({
45
+ name: attr('string'), // ember-data tracks this already
46
+ info: attr('object'), // ember-data does not track modifications
47
+ json: attr(), // ember-data does not track modifications if this is object
48
+ company: belongsTo('company', { async: false, polymorphic: true }), // ember-data does not track replacement
49
+ profile: belongsTo('profile', { async: true }), // ember-data does not track replacement
50
+ projects: hasMany('project', { async: false }), // ember-data does not track additions/deletions
51
+ pets: hasMany('pet', { async: true, polymorphic: true }) // ember-data does not track additions/deletions
52
+ });
53
+ ```
54
+
55
+ You can not currently rollback the info, json if they are modified
56
+ or company, profile, projects and pets if they change.
57
+
58
+
59
+ ### model changes
60
+
61
+ - The method ```modelChanges()``` is added to model
62
+ - Shows you any changes in an object attribute type
63
+ - whether modified or replacing the value
64
+ - attr() will default to 'object' type
65
+ - works with any custom type you have created
66
+ - Shows when you replace a belongsTo association
67
+ - Shows when you add to a hasMany association
68
+ - Shows when you delete from a hasMany association
69
+ - Merges ember-data `changeAttribute()` information into one unified change object
70
+ - Unlike ember-data no last and current value is shown, just the boolean => true
71
+ - Though you will see [last value, current value] for the attributes that ember-data tracks
72
+
73
+ Example: ( remove from a hasMany )
74
+ ```javascript
75
+ user.get('projects').removeObject(firstProject); // remove project1
76
+ user.modelChanges() //=> {projects: true }
77
+ ```
78
+
79
+
80
+ ### Rollback
81
+
82
+ - The method ```rollback()``` is added to model
83
+ - If you're not using auto track you have to call ```startTrack()``` before editing
84
+ - Performace wise, it's way faster than you think it should be.
85
+ - Tested on model with hundreds of items in a hasMany association.
86
+ - Though you might want to think twice when tracking one with thousands
87
+
88
+ Usage:
89
+
90
+ - make and makeList are from [ember-data-factory-guy](https://github.com/danielspaniel/ember-data-factory-guy).
91
+ - they create and push models ( based on factories ) into the ember-data store
92
+
93
+ ```javascript
94
+ let info = {foo: 1};
95
+ let projects = makeList('project', 2);
96
+ let [project1] = projects;
97
+ let pets = makeList('cat', 4);
98
+ let [cat, cat2] = pets;
99
+ let bigCompany = make('big-company');
100
+ let smallCompany = make('small-company');
101
+
102
+ let user = make('user', { profile: profile1, company: bigCompany, pets, projects });
103
+
104
+ // manual tracking model means you have to explicitly call => startTrack
105
+ // to save the current state of things before you edit
106
+ user.startTrack();
107
+
108
+ // edit things
109
+ user.setProperties({
110
+ 'info.foo': 3,
111
+ company: smallCompany,
112
+ profile: profile2,
113
+ projects: [project1],
114
+ pets: [cat1, cat2]
115
+ });
116
+
117
+ user.rollback();
118
+
119
+ // it's all back to the way it was
120
+ user.get('info') //=> {foo: 1}
121
+ user.get('profile') //=> profile1
122
+ user.get('company') //=> bigCompany
123
+ user.get('projects') //=> first 2 projects
124
+ user.get('pets') //=> back to the same 4 pets
125
+
126
+ ```
127
+
128
+ ### isDirty, hasDirtyRelations
129
+ - Computed properties to check if the model has changed
130
+ - Not enabled by default
131
+ - Need to set enableIsDirty ( true ) on model or global [configuration](https://github.com/danielspaniel/ember-data-change-tracker#configuration)
132
+ - The only attributes that can NOT be tracked with isDirty are object/array
133
+ attributes
134
+
135
+ Usage:
136
+
137
+ ```javascript
138
+
139
+ let info = {foo: 1};
140
+ let pets = makeList('cat', 4);
141
+ let [cat, cat2] = pets;
142
+ let bigCompany = make('big-company');
143
+ let smallCompany = make('small-company');
144
+
145
+ let user = make('user', { company: bigCompany, pets });
146
+
147
+ user.startTrack();
148
+
149
+ // edit things
150
+ user.set('name', "new name");
151
+ user.get('isDirty'); //=> true
152
+
153
+ user.rollback();
154
+ user.get('isDirty'); //=> false
155
+
156
+ user.set('company', smallCompany);
157
+ user.get('hasDirtyRelations'); //=> true
158
+ user.get('isDirty'); //=> true
159
+
160
+ user.rollback();
161
+ user.get('isDirty'); //=> false
162
+
163
+ user.set('pets', [cat, cat2]);
164
+ user.get('hasDirtyRelations'); //=> true
165
+ user.get('isDirty'); //=> true
166
+
167
+ user.rollback();
168
+ user.get('isDirty'); //=> false
169
+
170
+ // things that don't work
171
+ user.set('info.foo', 3);
172
+ user.get('isDirty'); //=> false ( object/array attributes don't work for computed isDirty )
173
+
174
+ ```
175
+
176
+ ### Configuration
177
+
178
+ - Global configuration
179
+ - By default the global settings are:
180
+ - { **trackHasMany**: *true*, **auto**: *false*, **enableIsDirty**: *false* }
181
+ - Essentially this says, track everything in the model but only when I tell you
182
+ - Since this is manual mode you probably want to track everything
183
+ since you are focused on one edit at a time, hence trackHasMany is on
184
+ - The options available are:
185
+ - **trackHasMany** : should hasMany associations be tracked? ( _true_ is default )
186
+ - this is just a shortcut to exclude all the hasMany relations
187
+ - **auto** : should tracking be turned on by default? ( _false_ is default )
188
+ - auto tracking means when any model is saved/updated/reloaded the tracker will save
189
+ the current state, allowing you to rollback anytime
190
+ - **enableIsDirty** : sets up computed properties on a model
191
+ - ```hasDirtyRelations``` for checking on changed relationships
192
+ - ```isDirty``` for checking on any changes
193
+ - NOTE: not working for object type attributes, since those are too
194
+ difficult to observe for the purpose of computed properties
195
+
196
+ - Model configuration
197
+ - Takes precedence over global
198
+ - So, globally auto track could be off, but on one model you can turn it on
199
+ - The options available are:
200
+ - **trackHasMany** : same as global trackHasMany
201
+ - **auto** : same as global auto
202
+ - **only** : limit the attributes/associations tracked on this model to just these
203
+ - **except** : don't include these attributes/associations
204
+ - You can use 'only' and 'except' at the same time, but you could also clean your nose with a pipe cleaner
205
+
206
+ ```javascript
207
+ // file config/environment.js
208
+ var ENV = {
209
+ modulePrefix: 'dummy',
210
+ environment: environment,
211
+ rootURL: '/',
212
+ locationType: 'auto',
213
+ changeTracker: { trackHasMany: true, auto: true },
214
+ EmberENV: {
215
+ ... rest of config
216
+
217
+ ```
218
+ - Set options on the model
219
+
220
+ ```javascript
221
+ // file app/models/user.js
222
+ export default Model.extend({
223
+ changeTracker: {only: ['info', 'company', 'pets']}, // settings for user models
224
+
225
+ name: attr('string'),
226
+ info: attr('object'),
227
+ json: attr(),
228
+ company: belongsTo('company', { async: false, polymorphic: true }),
229
+ profile: belongsTo('profile', { async: true }),
230
+ projects: hasMany('project', { async: false }),
231
+ pets: hasMany('pet', { async: true, polymorphic: true })
232
+ });
233
+ ```
234
+
235
+ ### Serializer extras
236
+ - Mixin is provided that will allow you to remove any attributes/associations
237
+ that did not change from the serialized json
238
+ - Useful when you want to reduce the size of a json payload
239
+ - removing unchanged values can be big reduction at times
240
+
241
+ Example:
242
+
243
+ Let's say you set up the user model's serializer with keep-only-changed mixin
244
+
245
+ ```javascript
246
+ // file: app/serializers/user.js
247
+ import DS from 'ember-data';
248
+ import keepOnlyChanged from 'ember-data-change-tracker/mixins/keep-only-changed';
249
+
250
+ export default DS.RESTSerializer.extend(keepOnlyChanged);
251
+ ```
252
+
253
+ Then when you are updating the user model
254
+
255
+ ```javascript
256
+ user.set('info.foo', 1);
257
+ user.serialize(); //=> '{ info: {"foo:1"} }'
258
+ ```
259
+
260
+ Without this mixin enabled the json would look like:
261
+ ```javascript
262
+ { name: "dude", info: {"foo:1"}, company: "1" companyType: "company", profile: "1" }
263
+ ```
264
+ where all the attributes and association are included whether they changed or not
265
+
266
+
267
+ ## Extra's
268
+ - Adds a few more helpful methods to ember data model
269
+ - ```didChange(key) ```
270
+ - did the value on this key change?
271
+ - ```savedTrackerValue(key)```
272
+ - this is the value that the key had after it was created/saved and
273
+ before any modifications
274
+
275
+ Usage:
276
+ ```javascript
277
+ user.startTrack(); // saves all keys that are being tracked
278
+ user.savedTrackerValue('info') //=> {foo: 1} original value of info
279
+ user.set('info.foo', 8)
280
+ user.didChange('info') //=> true
281
+ user.savedTrackerValue('info') //=> {foo: 1} original value of info
282
+ ```
283
+
284
+ ## Known Issues
285
+ - When pushing data to the store directly to create a model ( usually done when using
286
+ websockets .. but same issue if using factory guy) you need to call ```model.saveTrackerChanges()```
287
+ manually after creating that new model
288
+ - Testing
289
+ - In unit / integration tests you have to manually initialize change-tracker
290
+ if you are testing anything that requires the addon to be enabled
291
+
292
+ For example:
293
+
294
+ ```javascript
295
+
296
+ import {moduleForModel, test} from 'ember-qunit';
297
+ import {make, manualSetup} from 'ember-data-factory-guy';
298
+ import {initializer as changeInitializer} from 'ember-data-change-tracker';
299
+
300
+ moduleForModel('project', 'Unit | Model | project', {
301
+
302
+ beforeEach() {
303
+ manualSetup(this.container);
304
+ changeInitializer();
305
+ }
306
+ });
307
+
308
+ ```
package/addon/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import {initializer} from './initializer';
2
+ import keepOnlyChanged from './mixins/keep-only-changed';
3
+
4
+ export {keepOnlyChanged, initializer};
@@ -0,0 +1,4 @@
1
+ /* global require */
2
+ export function initializer() {
3
+ require('ember-data-change-tracker/model-ext');
4
+ }
@@ -0,0 +1,26 @@
1
+ import Ember from 'ember';
2
+
3
+ // EmberData does not serialize hasMany relationships by default
4
+ export default Ember.Mixin.create({
5
+ keepValue(record, key) {
6
+ return record.get('isNew') || record.didChange(key);
7
+ },
8
+
9
+ serializeAttribute: function(snapshot, json, key) {
10
+ if (this.keepValue(snapshot.record, key)) {
11
+ return this._super(...arguments);
12
+ }
13
+ },
14
+
15
+ serializeBelongsTo: function(snapshot, json, relationship) {
16
+ if (this.keepValue(snapshot.record, relationship.key)) {
17
+ return this._super(...arguments);
18
+ }
19
+ },
20
+
21
+ serializeHasMany: function(snapshot, json, relationship) {
22
+ if (this.keepValue(snapshot.record, relationship.key)) {
23
+ return this._super(...arguments);
24
+ }
25
+ }
26
+ });
@@ -0,0 +1,212 @@
1
+ import Ember from 'ember';
2
+ import Model from 'ember-data/model';
3
+ import Tracker from './tracker';
4
+
5
+ Model.reopen({
6
+
7
+ init(){
8
+ this._super(...arguments);
9
+ if (Tracker.isAutoSaveEnabled(this)) {
10
+ this.initTracking();
11
+ }
12
+ if (Tracker.isIsDirtyEnabled(this)) {
13
+ // this is experimental
14
+ Tracker.initializeDirtiness(this);
15
+ }
16
+
17
+ this.setupTrackerMetaData();
18
+ this.setupUnknownRelationshipLoadObservers();
19
+ },
20
+
21
+ /**
22
+ * Did an attribute/association change?
23
+ *
24
+ * @param {String} key the attribute/association name
25
+ * @param {Object} changed optional ember-data changedAttribute object
26
+ * @returns {Boolean} true if value changed
27
+ */
28
+ didChange(key, changed, options) {
29
+ return Tracker.didChange(this, key, changed, options);
30
+ },
31
+
32
+ /**
33
+ * Did any attribute/association change?
34
+ *
35
+ * returns object with:
36
+ * {key: value} = {attribute: true}
37
+ *
38
+ * If the the attribute changed, it will be included in this object
39
+ *
40
+ * @returns {*}
41
+ */
42
+ modelChanges() {
43
+ let changed = Ember.assign({}, this.changedAttributes());
44
+ let trackerInfo = Tracker.metaInfo(this);
45
+ for (let key in trackerInfo) {
46
+ if (!changed[key] && trackerInfo.hasOwnProperty(key)) {
47
+ if (this.didChange(key, changed)) {
48
+ changed[key] = true;
49
+ }
50
+ }
51
+ }
52
+ return changed;
53
+ },
54
+
55
+ /**
56
+ * Rollback all the changes on this model, for the keys you are
57
+ * tracking.
58
+ *
59
+ * NOTE: Be sure you understand what keys you are tracking.
60
+ * By default, tracker will save all keys, but if you set up
61
+ * a model to 'only' track a limited set of keys, then the rollback
62
+ * will only be limited to those keys
63
+ *
64
+ */
65
+ rollback() {
66
+ const isNew = this.get('isNew');
67
+ this.rollbackAttributes();
68
+ if (isNew) { return; }
69
+ let trackerInfo = Tracker.metaInfo(this);
70
+ let rollbackData = Tracker.rollbackData(this, trackerInfo);
71
+ let normalized = Tracker.normalize(this, rollbackData);
72
+ this.store.push(normalized);
73
+ },
74
+
75
+ // alias for saveChanges method
76
+ startTrack() {
77
+ this.initTracking();
78
+ this.saveChanges();
79
+ },
80
+
81
+ // Ember Data DS.Model events
82
+ // http://api.emberjs.com/ember-data/3.10/classes/DS.Model/events
83
+ //
84
+ // Replaces deprecated Ember.Evented usage:
85
+ // https://github.com/emberjs/rfcs/blob/master/text/0329-deprecated-ember-evented-in-ember-data.md
86
+ // Related: https://github.com/emberjs/rfcs/pull/329
87
+
88
+ onIsNewChanged() {
89
+ if (this.isNew === false) {
90
+ this.saveOnCreate()
91
+ this.removeObserver('isNew', this, this.onIsNewChanged);
92
+ }
93
+ },
94
+
95
+ onIsDeletedChanged() {
96
+ if (this.isDeleted === true) {
97
+ this.clearSavedAttributes();
98
+ this.removeObserver('isDeleted', this, this.onIsDeletedChanged);
99
+ }
100
+ },
101
+
102
+ onIsLoadedChanged() {
103
+ this.setupTrackerMetaData();
104
+ this.setupUnknownRelationshipLoadObservers();
105
+ this.removeObserver('isLoaded', this, this.onIsLoadedChanged);
106
+ },
107
+
108
+ initTracking(){
109
+ // sync tracker with model on events like create/update/delete/load
110
+ if (this.isNew) {
111
+ this.addObserver('isNew', this, this.onIsNewChanged)
112
+ }
113
+ this.addObserver('isDeleted', this, this.onIsDeletedChanged)
114
+
115
+ if (!this.isLoaded) {
116
+ this.addObserver('isLoaded', this, this.onIsLoadedChanged);
117
+ }
118
+
119
+ // there is no didUpdate hook anymore and no appropriate model props to base on
120
+ // saveOnUpdate should be called after model has been saved
121
+ // right after model
122
+ Tracker.setupTracking(this);
123
+ },
124
+
125
+ /**
126
+ * Save the current state of the model
127
+ *
128
+ * NOTE: This is needed when manually pushing data
129
+ * to the store and ussing Ember < 2.10
130
+ *
131
+ * options like => {except: 'company'}
132
+ *
133
+ * @param {Object} options
134
+ */
135
+ saveChanges(options) {
136
+ Tracker.setupTracking(this);
137
+ Tracker.saveChanges(this, options);
138
+ Tracker.triggerIsDirtyReset(this);
139
+ },
140
+
141
+
142
+ saveTrackerChanges(options) {
143
+ this.saveChanges(options);
144
+ },
145
+
146
+ /**
147
+ * Get value of the last known value tracker is saving for this key
148
+ *
149
+ * @param {String} key attribute/association name
150
+ * @returns {*}
151
+ */
152
+ savedTrackerValue(key) {
153
+ return Tracker.lastValue(this, key);
154
+ },
155
+
156
+ // save state when model is loaded or created if using auto save
157
+ setupTrackerMetaData() {
158
+ if (Tracker.isIsDirtyEnabled(this)) {
159
+ // this is experimental
160
+ Tracker.initializeDirtiness(this);
161
+ }
162
+ if (Tracker.isAutoSaveEnabled(this)) {
163
+ this.saveChanges();
164
+ }
165
+ },
166
+
167
+ // watch for relationships loaded with data via links
168
+ setupUnknownRelationshipLoadObservers() {
169
+ this.eachRelationship((key) => {
170
+ this.addObserver(key, this, 'observeUnknownRelationshipLoaded');
171
+ });
172
+ },
173
+
174
+ // when model updates, update the tracked state if using auto save
175
+ saveOnUpdate() {
176
+ if (Tracker.isAutoSaveEnabled(this) || Tracker.isIsDirtyEnabled(this)) {
177
+ this.saveChanges();
178
+ }
179
+ },
180
+
181
+ // when model creates, update the tracked state if using auto save
182
+ saveOnCreate() {
183
+ if (Tracker.isAutoSaveEnabled(this) || Tracker.isIsDirtyEnabled(this)) {
184
+ this.saveChanges();
185
+ }
186
+ },
187
+
188
+ // There is no didReload callback on models, so have to override reload
189
+ reload() {
190
+ let promise = this._super(...arguments);
191
+ promise.then(() => {
192
+ if (Tracker.isAutoSaveEnabled(this)) {
193
+ this.saveChanges();
194
+ }
195
+ });
196
+ return promise;
197
+ },
198
+
199
+ // when model deletes, remove any tracked state
200
+ clearSavedAttributes() {
201
+ Tracker.clear(this);
202
+ },
203
+
204
+ observeUnknownRelationshipLoaded(sender, key/*, value, rev*/) {
205
+ if (Tracker.trackingIsSetup(this) && Tracker.isTracking(this, key)) {
206
+ let saved = Tracker.saveLoadedRelationship(this, key);
207
+ if (saved) {
208
+ this.removeObserver(key, this, 'observeUnknownRelationshipLoaded');
209
+ }
210
+ }
211
+ }
212
+ });