@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 +15 -0
- package/LICENSE.md +9 -0
- package/README.md +308 -0
- package/addon/index.js +4 -0
- package/addon/initializer.js +4 -0
- package/addon/mixins/keep-only-changed.js +26 -0
- package/addon/model-ext.js +212 -0
- package/addon/tracker.js +570 -0
- package/addon/transforms/json.js +20 -0
- package/addon/transforms/object.js +20 -0
- package/addon/utilities.js +92 -0
- package/app/initializers/ember-data-change-tracker.js +7 -0
- package/app/mixins/change-serializer.js +1 -0
- package/app/transforms/json.js +1 -0
- package/app/transforms/object.js +1 -0
- package/config/environment.js +6 -0
- package/index.js +6 -0
- package/package.json +65 -0
package/addon/tracker.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
export { default } from '@cityads/ember-data-change-tracker/mixins/keep-only-changed';
|