@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.
@@ -0,0 +1,570 @@
1
+ import Ember from 'ember';
2
+ import { didModelChange, didModelsChange, relationShipTransform, relationshipKnownState } from './utilities';
3
+
4
+ const assign = Ember.assign || Ember.merge;
5
+ export const ModelTrackerKey = '-change-tracker';
6
+ export const RelationshipsKnownTrackerKey = '-change-tracker-relationships-known';
7
+ const alreadyTrackedRegex = /^-mf-|string|boolean|date|^number$/,
8
+ knownTrackerOpts = Ember.A(['only', 'auto', 'except', 'trackHasMany', 'enableIsDirty']),
9
+ defaultOpts = {trackHasMany: true, auto: false, enableIsDirty: false};
10
+
11
+ /**
12
+ * Helper class for change tracking models
13
+ */
14
+ export default class Tracker {
15
+
16
+ /**
17
+ * Get Ember application container
18
+ *
19
+ * @param {DS.Model} model
20
+ * @returns {*}
21
+ */
22
+ static container(model) {
23
+ return Ember.getOwner ? Ember.getOwner(model.store) : model.store.container;
24
+ }
25
+
26
+ /**
27
+ * Get tracker configuration from Ember application configuration
28
+ *
29
+ * @param {DS.Model} model
30
+ * @returns {*|{}}
31
+ */
32
+ static envConfig(model) {
33
+ let config = this.container(model).resolveRegistration('config:environment');
34
+ // sometimes the config is not available ?? not sure why
35
+ return config && config.changeTracker || {};
36
+ }
37
+
38
+ /**
39
+ * Get tracker configuration that is set on the model
40
+ *
41
+ * @param {DS.Model} model
42
+ * @returns {*|{}}
43
+ */
44
+ static modelConfig(model) {
45
+ return model.changeTracker || {};
46
+ }
47
+
48
+ /**
49
+ * Is this model in auto save mode?
50
+ *
51
+ * @param model
52
+ * @returns {Boolean}
53
+ */
54
+ static isAutoSaveEnabled(model) {
55
+ if (model.constructor.trackerAutoSave === undefined) {
56
+ let options = this.options(model);
57
+ model.constructor.trackerAutoSave = options.auto;
58
+ }
59
+ return model.constructor.trackerAutoSave;
60
+ }
61
+
62
+ /**
63
+ * Is this model have isDirty option enabled?
64
+ *
65
+ * @param model
66
+ * @returns {Boolean}
67
+ */
68
+ static isIsDirtyEnabled(model) {
69
+ if (model.constructor.trackerEnableIsDirty === undefined) {
70
+ let options = this.options(model);
71
+ model.constructor.trackerEnableIsDirty = options.enableIsDirty;
72
+ }
73
+ return model.constructor.trackerEnableIsDirty;
74
+ }
75
+
76
+ /**
77
+ * A custom attribute should have a transform function associated with it.
78
+ * If not, use object transform.
79
+ *
80
+ * A transform function is required for serializing and deserializing
81
+ * the attribute in order to save past values and then renew them on rollback
82
+ *
83
+ * @param {DS.Model} model
84
+ * @param {String} attributeType like: 'object', 'json' or could be undefined
85
+ * @returns {*}
86
+ */
87
+ static transformFn(model, attributeType) {
88
+ let transformType = attributeType || 'object';
89
+ return this.container(model).lookup(`transform:${transformType}`);
90
+ }
91
+
92
+ /**
93
+ * The rollback data will be an object with keys as attribute and relationship names
94
+ * with values for those keys.
95
+ *
96
+ * For example:
97
+ *
98
+ * { id: 1, name: 'Acme Inc', company: 1, pets: [1,2] }
99
+ *
100
+ * Basically a REST style payload. So, convert that to JSONAPI so it can be
101
+ * pushed to the store
102
+ *
103
+ * @param {DS.Model} model
104
+ * @param {Object} data rollback data
105
+ */
106
+ static normalize(model, data) {
107
+ let container = this.container(model);
108
+ let serializer = container.lookup('serializer:-rest');
109
+ serializer.set('store', model.store);
110
+ return serializer.normalize(model.constructor, data);
111
+ }
112
+
113
+ /**
114
+ * Find the meta data for all keys or a single key (attributes/association)
115
+ * that tracker is tracking on this model
116
+ *
117
+ * @param {DS.Model} model
118
+ * @param {string} [key] only this key's info and no other
119
+ * @returns {*} all the meta info on this model that tracker is tracking
120
+ */
121
+ static metaInfo(model, key = null) {
122
+ let info = (model.constructor.trackerKeys || {});
123
+ if (key) {
124
+ return info[key];
125
+ }
126
+ return info;
127
+ }
128
+
129
+ /**
130
+ * Find whether this key is currently being tracked.
131
+ *
132
+ * @param {DS.Model} model
133
+ * @param {string} [key]
134
+ * @returns {boolean} true if this key is being tracked. false otherwise
135
+ */
136
+ static isTracking(model, key) {
137
+ let info = (model.constructor.trackerKeys || {});
138
+ return !!info[key];
139
+ }
140
+
141
+ /**
142
+ * On the model you can set options like:
143
+ *
144
+ * changeTracker: {auto: true}
145
+ * changeTracker: {auto: true, enableIsDirty: true}
146
+ * changeTracker: {auto: true, only: ['info']}
147
+ * changeTracker: {except: ['info']}
148
+ * changeTracker: {except: ['info'], trackHasMany: true}
149
+ *
150
+ * In config environment you can set options like:
151
+ *
152
+ * changeTracker: {auto: true, trackHasMany: false, enableIsDirty: true}
153
+ * // default is: {auto: false, trackHasMany: true, enableIsDirty: false}
154
+ *
155
+ * The default is set to trackHasMany but not auto track, since
156
+ * that is the most do nothing approach and when you do call `model.startTrack()`
157
+ * it is assumed you want to track everything.
158
+ *
159
+ * Also, by default the isDirty computed property is not setup. You have to enable
160
+ * it globally or on a model
161
+ *
162
+ * @param {DS.Model} model
163
+ * @returns {*}
164
+ */
165
+ static options(model) {
166
+ let envConfig = this.envConfig(model);
167
+ let modelConfig = this.modelConfig(model);
168
+ let opts = assign({}, defaultOpts, envConfig, modelConfig);
169
+
170
+ let unknownOpts = Object.keys(opts).filter((v) => !knownTrackerOpts.includes(v));
171
+ Ember.assert(`[ember-data-change-tracker] changeTracker options can have
172
+ 'only', 'except' , 'auto', 'enableIsDirty' or 'trackHasMany' but you are declaring: ${unknownOpts}`,
173
+ Ember.isEmpty(unknownOpts)
174
+ );
175
+
176
+ return opts;
177
+ }
178
+
179
+ // has tracking already been setup on this model?
180
+ static trackingIsSetup(model) {
181
+ return model.constructor.alreadySetupTrackingMeta;
182
+ }
183
+
184
+ /**
185
+ * Setup tracking meta data for this model,
186
+ * unless it's already been setup
187
+ *
188
+ * @param {DS.Model} model
189
+ */
190
+ static setupTracking(model) {
191
+ if (!this.trackingIsSetup(model)) {
192
+ model.constructor.alreadySetupTrackingMeta = true;
193
+ let info = Tracker.getTrackerInfo(model);
194
+ model.constructor.trackerKeys = info.keyMeta;
195
+ model.constructor.trackerAutoSave = info.autoSave;
196
+ model.constructor.trackerEnableIsDirty = info.enableIsDirty;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get the tracker meta data associated with this model
202
+ *
203
+ * @param {DS.Model} model
204
+ * @returns {{autoSave, keyMeta: {}}}
205
+ */
206
+ static getTrackerInfo(model) {
207
+ let [trackableInfo, hasManyList] = this.extractKeys(model);
208
+ let trackerOpts = this.options(model);
209
+
210
+ let all = Object.keys(trackableInfo);
211
+ let except = trackerOpts.except || [];
212
+ let only = trackerOpts.only || [...all];
213
+
214
+ if (!trackerOpts.trackHasMany) {
215
+ except = [...except, ...hasManyList];
216
+ }
217
+
218
+ all = [...all].filter(a => !except.includes(a));
219
+ all = [...all].filter(a => only.includes(a));
220
+
221
+ let keyMeta = {};
222
+ Object.keys(trackableInfo).forEach(key => {
223
+ if (all.includes(key)) {
224
+ let info = trackableInfo[key];
225
+ info.transform = this.getTransform(model, key, info);
226
+ keyMeta[key] = info;
227
+ }
228
+ });
229
+
230
+ let {enableIsDirty} = trackerOpts;
231
+ return {autoSave: trackerOpts.auto, enableIsDirty, keyMeta};
232
+ }
233
+
234
+ /**
235
+ * Go through the models attributes and relationships so see
236
+ * which of these keys could be trackable
237
+ *
238
+ * @param {DS.Model} model
239
+ * @returns {[*,*]} meta data about possible keys to track
240
+ */
241
+ static extractKeys(model) {
242
+ let {constructor} = model;
243
+ let trackerKeys = {};
244
+ let hasManyList = [];
245
+
246
+ constructor.eachAttribute((attribute, meta) => {
247
+ if (!alreadyTrackedRegex.test(meta.type)) {
248
+ trackerKeys[attribute] = {type: 'attribute', name: meta.type};
249
+ }
250
+ });
251
+
252
+ constructor.eachRelationship((key, relationship) => {
253
+ trackerKeys[key] = {
254
+ type: relationship.kind,
255
+ polymorphic: relationship.options.polymorphic,
256
+ knownState: relationshipKnownState[relationship.kind]
257
+ };
258
+ if (relationship.kind === 'hasMany') {
259
+ hasManyList.push(key);
260
+ }
261
+ });
262
+
263
+ return [trackerKeys, hasManyList];
264
+ }
265
+
266
+ /**
267
+ * Get the transform for an attribute or association.
268
+ * The attribute transforms are held by ember-data, and
269
+ * the tracker uses custom transform for relationships
270
+ *
271
+ * @param {DS.Model} model
272
+ * @param {String} key attribute/association name
273
+ * @param {Object} info tracker meta data for this key
274
+ * @returns {*}
275
+ */
276
+ static getTransform(model, key, info) {
277
+ let transform;
278
+
279
+ if (info.type === 'attribute') {
280
+ transform = this.transformFn(model, info.name);
281
+
282
+ Ember.assert(`[ember-data-change-tracker] changeTracker could not find
283
+ a ${info.name} transform function for the attribute '${key}' in
284
+ model '${model.constructor.modelName}'.
285
+ If you are in a unit test, be sure to include it in the list of needs`,
286
+ transform
287
+ );
288
+ } else {
289
+ transform = relationShipTransform[info.type];
290
+ }
291
+
292
+ return transform;
293
+ }
294
+
295
+ /**
296
+ * Did the key change since the last time state was saved?
297
+ *
298
+ * @param {DS.Model} model
299
+ * @param {String} key attribute/association name
300
+ * @param {Object} [changed] changed object
301
+ * @param {Object} [info] model tracker meta data object
302
+ * @returns {*}
303
+ */
304
+ static didChange(model, key, changed, info) {
305
+ changed = changed || model.changedAttributes();
306
+ if (changed[key]) {
307
+ return true;
308
+ }
309
+ let keyInfo = info && info[key] || this.metaInfo(model, key);
310
+ if (keyInfo) {
311
+ let current = this.serialize(model, key, keyInfo);
312
+ let last = this.lastValue(model, key);
313
+ switch (keyInfo.type) {
314
+ case 'attribute':
315
+ case 'belongsTo':
316
+ return didModelChange(current, last, keyInfo.polymorphic);
317
+ case 'hasMany':
318
+ return didModelsChange(current, last, keyInfo.polymorphic);
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Serialize the value to be able to tell if the value changed.
325
+ *
326
+ * For attributes, using the transform function that each custom
327
+ * attribute should have.
328
+ *
329
+ * For belongsTo, and hasMany using using custom transform
330
+ *
331
+ * @param {DS.Model} model
332
+ * @param {String} key attribute/association name
333
+ */
334
+ static serialize(model, key, keyInfo) {
335
+ let info = keyInfo || this.metaInfo(model, key);
336
+ let value;
337
+ if (info.type === 'attribute') {
338
+ value = info.transform.serialize(model.get(key));
339
+ if (typeof value !== 'string') {
340
+ value = JSON.stringify(value);
341
+ }
342
+ } else {
343
+ value = info.transform.serialize(model, key, info);
344
+ }
345
+ return value;
346
+ }
347
+
348
+ /**
349
+ * Determine if the key represents data that the client knows about.
350
+ *
351
+ * For relationships that are async links it may be that they are yet to be
352
+ * loaded and so a determination of 'changed' cannot be known
353
+ *
354
+ * @param {DS.Model} model
355
+ * @param {String} key attribute/association name
356
+ */
357
+ static isKnown(model, key, keyInfo) {
358
+ let info = keyInfo || this.metaInfo(model, key);
359
+ let value;
360
+ if (info.type === 'attribute') {
361
+ value = true;
362
+ } else {
363
+ value = info.knownState.isKnown(model, key);
364
+ }
365
+ return value;
366
+ }
367
+
368
+ /**
369
+ * Retrieve the last known value for this model key
370
+ *
371
+ * @param {DS.Model} model
372
+ * @param {String} key attribute/association name
373
+ * @returns {*}
374
+ */
375
+ static lastValue(model, key) {
376
+ return (model.get(ModelTrackerKey) || {})[key];
377
+ }
378
+
379
+ /**
380
+ * Retrieve the last known state for this model key
381
+ *
382
+ * @param {DS.Model} model
383
+ * @param {String} key attribute/association name
384
+ * @returns {*}
385
+ */
386
+ static lastKnown(model, key) {
387
+ return (model.get(RelationshipsKnownTrackerKey) || {})[key];
388
+ }
389
+
390
+ /**
391
+ * Gather all the rollback data
392
+ *
393
+ * @param {DS.Model} model
394
+ * @param trackerInfo
395
+ * @returns {{*}}
396
+ */
397
+ static rollbackData(model, trackerInfo) {
398
+ let data = {id: model.id};
399
+ Object.keys(trackerInfo).forEach((key) => {
400
+ let keyInfo = trackerInfo[key];
401
+ if (this.didChange(model, key, null, trackerInfo)) {
402
+ // For now, blow away the hasMany relationship before resetting it
403
+ // since just pushing new data is not resetting the relationship.
404
+ // This slows down the hasMany rollback by about 25%, but still
405
+ // fast => (~100ms) with 500 items in a hasMany
406
+ if (keyInfo.type === 'hasMany') {
407
+ model.set(key, []);
408
+ }
409
+ let lastValue = Tracker.lastValue(model, key);
410
+ if (keyInfo.type === 'attribute' && !keyInfo.name) { // attr() undefined type
411
+ lastValue = keyInfo.transform.deserialize(lastValue);
412
+ }
413
+ data[key] = lastValue;
414
+ }
415
+ });
416
+ return data;
417
+ }
418
+
419
+ /**
420
+ * Save change tracker attributes
421
+ *
422
+ * @param {DS.Model} model
423
+ * @param {Object} options
424
+ * except array of keys to exclude
425
+ */
426
+ static saveChanges(model, {except = []} = {}) {
427
+ let metaInfo = this.metaInfo(model);
428
+ let keys = Object.keys(metaInfo).filter(key => !except.includes(key));
429
+ Tracker.saveKeys(model, keys);
430
+ }
431
+
432
+ /**
433
+ * Save the current relationship value into the hash only if it was previously
434
+ * unknown (i.e. to be loaded async via a link)
435
+ *
436
+ * @param {DS.Model} model
437
+ * @param {String} key association name
438
+ * @returns {boolean} true if the current relationship value was saved, false otherwise
439
+ */
440
+ static saveLoadedRelationship(model, key) {
441
+ let saved = false;
442
+ if (!Tracker.lastKnown(model, key)) {
443
+ let keyInfo = this.metaInfo(model, key);
444
+ if (Tracker.isKnown(model, key, keyInfo)) {
445
+ Tracker.saveKey(model, key);
446
+ saved = true;
447
+ }
448
+ }
449
+ return saved;
450
+ }
451
+
452
+ /**
453
+ * Manually trigger the isDirty properties to refresh themselves
454
+ *
455
+ * @param {DS.Model} model
456
+ */
457
+ static triggerIsDirtyReset(model) {
458
+ model.notifyPropertyChange('hasDirtyAttributes');
459
+ model.notifyPropertyChange('hasDirtyRelations');
460
+ }
461
+
462
+ /**
463
+ * Save the value from an array of keys model's tracker hash
464
+ * and save the relationship states if keys represents a relationship
465
+ *
466
+ * @param {DS.Model} model
467
+ * @param {Array} keys to save
468
+ */
469
+
470
+ static saveKeys(model, keys){
471
+ let modelTracker = model.get(ModelTrackerKey) || {},
472
+ relationshipsKnownTracker = model.get(RelationshipsKnownTrackerKey) || {},
473
+ isNew = model.get('isNew');
474
+
475
+ keys.forEach(key => {
476
+ modelTracker[key] = isNew ? undefined : this.serialize(model, key);
477
+ relationshipsKnownTracker[key] = isNew ? true : this.isKnown(model, key);
478
+ })
479
+ model.setProperties({[ModelTrackerKey]:modelTracker, [RelationshipsKnownTrackerKey]: relationshipsKnownTracker})
480
+ }
481
+
482
+ /**
483
+ * Save current model key value in model's tracker hash
484
+ * and save the relationship state if key represents a relationship
485
+ *
486
+ * @param {DS.Model} model
487
+ * @param {String} key attribute/association name
488
+ */
489
+ static saveKey(model, key) {
490
+ this.saveKeys(model, [key]);
491
+ }
492
+
493
+ /**
494
+ * Remove tracker hashes from the model's state
495
+ *
496
+ * @param {DS.Model} model
497
+ */
498
+ static clear(model) {
499
+ model.set(ModelTrackerKey, undefined);
500
+ model.set(RelationshipsKnownTrackerKey, undefined);
501
+ }
502
+
503
+ /**
504
+ * Set up the computed properties:
505
+ *
506
+ * 'isDirty', 'hasDirtyAttributes', 'hasDirtyRelations'
507
+ *
508
+ * only if the application or model configuration has opted into
509
+ * enable these properties, with the enableIsDirty flag
510
+ *
511
+ * @param {DS.Model} model
512
+ */
513
+ static initializeDirtiness(model) {
514
+ const relations = [];
515
+ const relationsObserver = [];
516
+ const attrs = [];
517
+
518
+ model.eachRelationship((name, descriptor) => {
519
+ if (descriptor.kind === 'hasMany') {
520
+ relations.push(descriptor.key);
521
+ if (descriptor.options.async) {
522
+ relationsObserver.push(descriptor.key + '.content.@each.id');
523
+ } else {
524
+ relationsObserver.push(descriptor.key + '.@each.id');
525
+ }
526
+ } else {
527
+ relations.push(descriptor.key);
528
+ relationsObserver.push(descriptor.key + '.content');
529
+ }
530
+ });
531
+
532
+ model.eachAttribute(name => {
533
+ return attrs.push(name);
534
+ });
535
+
536
+ const hasDirtyRelations = function() {
537
+ const changed = model.modelChanges();
538
+ return !!relations.find(key => changed[key]);
539
+ };
540
+
541
+ const hasDirtyAttributes = function() {
542
+ const changed = model.modelChanges();
543
+ return !!attrs.find(key => changed[key]);
544
+ };
545
+
546
+ const isDirty = function() {
547
+ return model.get('hasDirtyAttributes') || model.get('hasDirtyRelations');
548
+ };
549
+
550
+ Ember.defineProperty(
551
+ model,
552
+ 'hasDirtyAttributes',
553
+ Ember.computed.apply(Ember, attrs.concat([hasDirtyAttributes]))
554
+ );
555
+
556
+ Ember.defineProperty(
557
+ model,
558
+ 'hasDirtyRelations',
559
+ Ember.computed.apply(Ember, relationsObserver.concat([hasDirtyRelations]))
560
+ );
561
+
562
+ Ember.defineProperty(
563
+ model,
564
+ 'isDirty',
565
+ Ember.computed.apply(Ember, ['hasDirtyAttributes', 'hasDirtyRelations', isDirty])
566
+ );
567
+
568
+ }
569
+
570
+ }
@@ -0,0 +1,20 @@
1
+ import Transform from "ember-data/transform";
2
+ /**
3
+ * This transform does not serializes to string,
4
+ * with JSON.stringify, but leaves the object as is.
5
+ *
6
+ * The data often does not need to be stringified
7
+ * so it's a valid case
8
+ */
9
+ export default Transform.extend({
10
+ serialize: function(value) {
11
+ return value;
12
+ },
13
+
14
+ deserialize: function(json) {
15
+ if (typeof json === "string") {
16
+ json = JSON.parse(json);
17
+ }
18
+ return json;
19
+ }
20
+ });
@@ -0,0 +1,20 @@
1
+ import DS from 'ember-data';
2
+ import Ember from 'ember';
3
+
4
+ export default DS.Transform.extend({
5
+ serialize: function(value) {
6
+ return value && JSON.stringify(value);
7
+ },
8
+
9
+ deserialize: function(value) {
10
+ if (Ember.isEmpty(value)) {
11
+ return {};
12
+ }
13
+ if (Ember.typeOf(value) === "object") {
14
+ return value;
15
+ }
16
+ if (Ember.typeOf(value) === 'string') {
17
+ return JSON.parse(value);
18
+ }
19
+ }
20
+ });
@@ -0,0 +1,92 @@
1
+ import Ember from 'ember';
2
+
3
+ export const modelTransform = function(model, polymorphic) {
4
+ if (polymorphic) {
5
+ return { id: model.id, type: model.modelName || model.constructor.modelName };
6
+ }
7
+ return model.id;
8
+ };
9
+
10
+ export const relationShipTransform = {
11
+ belongsTo: {
12
+ serialize(model, key, options) {
13
+ let relationship = model.belongsTo(key).belongsToRelationship;
14
+ let value = relationship.state.hasReceivedData ? relationship.localState: relationship.remoteState;
15
+ return value && modelTransform(value, options.polymorphic);
16
+ },
17
+
18
+ deserialize() {
19
+ }
20
+ },
21
+ hasMany: {
22
+ serialize(model, key, options) {
23
+ let relationship = model.hasMany(key).hasManyRelationship;
24
+ let value = relationship.localState;
25
+ return value && value.map(item => modelTransform(item, options.polymorphic));
26
+ },
27
+
28
+ deserialize() {
29
+ }
30
+ }
31
+ };
32
+
33
+ export const relationshipKnownState = {
34
+ belongsTo: {
35
+ isKnown(model, key) {
36
+ let belongsTo = model.belongsTo(key);
37
+ let relationship = belongsTo.belongsToRelationship;
38
+ return !relationship.state.isStale;
39
+ }
40
+ },
41
+ hasMany: {
42
+ isKnown(model, key) {
43
+ let hasMany = model.hasMany(key);
44
+ let relationship = hasMany.hasManyRelationship;
45
+ return !relationship.state.isStale;
46
+ }
47
+ }
48
+ };
49
+
50
+ export const isEmpty = function(value) {
51
+ if (Ember.typeOf(value) === 'object') {
52
+ return Object.keys(value).length === 0;
53
+ }
54
+ return Ember.isEmpty(value);
55
+ };
56
+
57
+ export const didSerializedModelChange = function(one, other, polymorphic) {
58
+ if (polymorphic) {
59
+ return one.id !== other.id || one.type !== other.type;
60
+ }
61
+ return one !== other;
62
+ };
63
+
64
+ export const didModelsChange = function(one, other, polymorphic) {
65
+ if (isEmpty(one) && isEmpty(other)) {
66
+ return false;
67
+ }
68
+
69
+ if ((one && one.length) !== (other && other.length)) {
70
+ return true;
71
+ }
72
+
73
+ for (let i = 0, len = one.length; i < len; i++) {
74
+ if (didSerializedModelChange(one[i], other[i], polymorphic)) {
75
+ return true;
76
+ }
77
+ }
78
+
79
+ return false;
80
+ };
81
+
82
+ export const didModelChange = function(one, other, polymorphic) {
83
+ if (isEmpty(one) && isEmpty(other)) {
84
+ return false;
85
+ }
86
+
87
+ if (!one && other || one && !other) {
88
+ return true;
89
+ }
90
+
91
+ return didSerializedModelChange(one, other, polymorphic);
92
+ };
@@ -0,0 +1,7 @@
1
+ import {initializer as initialize} from '@cityads/ember-data-change-tracker';
2
+
3
+ export default {
4
+ name: 'ember-data-change-tracker',
5
+ after: 'ember-data',
6
+ initialize
7
+ };
@@ -0,0 +1 @@
1
+ export { default } from '@cityads/ember-data-change-tracker/mixins/keep-only-changed';