Skip to content

Instantly share code, notes, and snippets.

@lifeart
Last active June 13, 2018 15:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lifeart/adf2da55b8c32650b5692c6b86b56047 to your computer and use it in GitHub Desktop.
Save lifeart/adf2da55b8c32650b5692c6b86b56047 to your computer and use it in GitHub Desktop.
Ember relationships rollback
const { Model, belongsTo, hasMany } = DS;
import { alias } from '@ember/object/computed';
import RollbackMixin from '../../mixins/rollbackable';


export default Model.extend(RollbackMixin, {
    rollabackable: computed(function(){
        return {
            hasMany: ['items', 'users'],
            belongsTo: ['country', 'user.pet']
        };
    }),
    pet: alias('user.pet'),
    users: hasMany('user'),
    items: hasMany('item'),
    country: belongsTo('country')
});
  • specify tracked relationships in rollabackable property.

Methods & Props

  • hasChanges() -> bool
  • resetChanges() -> reset changes
  • hasChangedAttributes -> bool -> hasMany relationships has changed attributes?

2 lvl deep belongsTo relationships trackable

If this mixin exists in children, rollback will be deeper

import Mixin from '@ember/object/mixin';
import { A } from '@ember/array';
import {
defineProperty,
computed
} from '@ember/object';
import {
addObserver,
removeObserver
} from '@ember/object/observers';
function resetItem(item) {
if ('resetChanges' in item) {
item.resetChanges();
} else {
if (item.get('hasDirtyAttributes')) {
item.rollbackAttributes();
}
}
}
function resetItems(keysToSet) {
Object.keys(keysToSet).forEach((propertyName)=>{
if (!keysToSet[propertyName]) {
return;
}
if (Array.isArray(keysToSet[propertyName])) {
keysToSet[propertyName].forEach(item=>{
resetItem(item);
})
} else {
resetItem(keysToSet[propertyName]);
}
});
}
function peekItemIfExists(property, ctx, store, id) {
if (id) {
let [firstPath, secondPath = false] = property.split('.');
let type = secondPath ? ctx.get(firstPath).relationshipFor(secondPath).type : ctx.relationshipFor(firstPath).type
let item = store.peekRecord(type, id);
if (!item) {
return false;
}
if (item.get('isDestroyed') || item.get('isDeleted')) {
console.error('Unable to restore deleted or destroyed item', item, property);
return false;
}
return item;
}
return false;
}
function getOrigin(ctx, modelChanges, keysToSet) {
let store = ctx.get('store');
Object.keys(modelChanges).forEach((property)=>{
let initialState = modelChanges[property].get('firstObject');
if (Array.isArray(initialState)) {
let items = A();
initialState.forEach((id)=>{
let item = peekItemIfExists(property, ctx, store, id);
if (item) {
items.push(item);
}
});
keysToSet[property] = items;
} else {
if (!initialState) {
keysToSet[property] = null;
} else {
let item = peekItemIfExists(property, ctx, store, initialState);
if (item) {
keysToSet[property] = item;
}
}
}
});
}
const debug = true;
const log = function() {
if (!debug) {
return;
}
console.log(...arguments);
}
const dirtyComputed = function(ctx, hasMany) {
let dirtyKey = 'hasDirtyAttributes';
let rels = hasMany.map((item) => `${item}.@each.${dirtyKey}`);
return computed.apply(ctx, [dirtyKey].concat(rels).concat(function () {
let items = this.getProperties([dirtyKey].concat(hasMany));
let dirtyResults = hasMany.reduce((results, relationshipName)=>{
return results.concat(items[relationshipName].mapBy('hasDirtyAttributes'));
}, A()).map((el)=>{
return {
result: el
};
});
log('dirtyResults', dirtyResults, this.modelChanges);
if (items.hasDirtyAttributes) {
return true;
}
return dirtyResults.isAny('result', true);
}))
}
export default Mixin.create({
init() {
this._super(...arguments);
this.modelChanges = {};
this._initTracker();
},
_initTracker() {
let {
hasMany
} = this.get('rollabackable');
let rels = this._relationshipsForObserver();
rels.forEach((rel) => {
addObserver(this, rel, this, 'onAnyRelationshipChanged');
});
defineProperty(this, 'hasChangedAttributes', dirtyComputed(this, hasMany));
},
destroy() {
let rels = this._relationshipsForObserver();
rels.forEach((rel)=>{
removeObserver(this, rel, this, 'onAnyRelationshipChanged');
});
this._super(...arguments);
},
_relationshipsForObserver() {
let {hasMany, belongsTo} = this.get('rollabackable');
let dirtyKey = 'id';
let rels = hasMany.map((item) => `${item}.@each.${dirtyKey}`).concat(
belongsTo.map((item) => `${item}.${dirtyKey}`)
);
return rels;
},
_addLastChanges() {
let rels = this._relationshipsForObserver();
rels.forEach((rel) => {
this.onAnyRelationshipChanged(this, rel);
});
},
ready() {
this._super(...arguments);
this._addLastChanges();
this.commitLastChanges();
},
//configurablePart, hasMany - list of hasMany keys to track, belongsTo - ...same;
rollabackable: computed(function(){
return {
hasMany: [],
belongsTo: [],
unloadNulls: false
};
}),
onAnyRelationshipChanged(model, key){
// console.log('onAnyRelationshipChanged', model, key, model.get(key));
let hasManyId = '.@each.id';
let belongsToId = '.id';
let value = null;
if (key.endsWith(hasManyId)) {
value = model.get(key.split('.')[0]).mapBy('id');
} else if (key.endsWith(belongsToId)) {
value = model.get(key);
}
this.trackChanges(key.replace(hasManyId,'').replace(belongsToId,''), value);
},
hasChangedRelationships() {
let isAnyItemHasHistory = false;
Object.keys(this.modelChanges).forEach((key) => {
if (Array.isArray(this.modelChanges[key]) && this.modelChanges[key].length >= 2) {
let firstItem = this.modelChanges[key].get('firstObject');
let lastItem = this.modelChanges[key].get('lastObject');
if (Array.isArray(lastItem)) {
lastItem = JSON.stringify(lastItem);
}
if (Array.isArray(firstItem)) {
firstItem = JSON.stringify(firstItem);
}
let firstState = String(firstItem).valueOf();
let lastState = String(lastItem).valueOf();
if (firstState !== lastState) {
isAnyItemHasHistory = true;
}
}
});
return isAnyItemHasHistory;
},
hasChanges({validAttibutes=null}={}) {
let hasChangedAttributes = this.get('hasChangedAttributes');
if (typeof validAttibutes === 'object' && validAttibutes !== null) {
hasChangedAttributes = JSON.stringify(this.relationshipsAttributesChanges()) !== JSON.stringify(validAttibutes);
}
return hasChangedAttributes || this.hasChangedRelationships() || false;
},
commitChanges() {
Object.keys(this.modelChanges).forEach((key)=>{
if (Array.isArray(this.modelChanges[key])) {
if (this.modelChanges[key].length) {
this.modelChanges[key] = A([this.modelChanges[key].shift()]);
}
}
});
},
relationshipsAttributesChanges() {
let changedAttributes = {};
this.get('rollabackable.hasMany').forEach((relationshipName) => {
changedAttributes[relationshipName] = this.get(relationshipName).map(el => el.changedAttributes());
});
return changedAttributes;
},
commitLastChanges() {
Object.keys(this.modelChanges).forEach((key) => {
if (Array.isArray(this.modelChanges[key])) {
if (this.modelChanges[key].length) {
this.modelChanges[key] = A([this.modelChanges[key].pop()]);
}
}
});
},
resetChanges() {
let keysToSet = {};
getOrigin(this, this.modelChanges, keysToSet);
resetItems(keysToSet);
this.rollbackAttributes();
log('resetChanges', arguments, { ...this.modelChanges});
this.commitChanges();
this.setProperties(keysToSet);
},
didLoad() {
this._super(...arguments);
log('didLoad', arguments, {...this.modelChanges});
this._addLastChanges();
this.commitLastChanges();
},
didUpdate() {
this._super(...arguments);
log('didUpdate', arguments, { ...this.modelChanges});
this._addLastChanges();
this.commitLastChanges();
},
didCreate() {
this._super(...arguments);
log('didUpdate', arguments, { ...this.modelChanges});
this._addLastChanges();
this.commitLastChanges();
},
_normalizeValue(raw) {
if (Array.isArray(raw)) {
return JSON.stringify(raw);
}
if (isNaN(raw)) {
return null;
}
if (typeof raw === 'string') {
return raw;
}
if (typeof raw === 'number') {
return raw;
}
if (typeof raw === 'object') {
return raw;
}
if (typeof raw === 'boolean') {
return raw;
}
if (typeof raw === 'undefined') {
return undefined;
}
return raw;
},
trackChanges(key, rawValue) {
if (this.get('isNew')) {
return;
}
let value = String(this._normalizeValue(rawValue)).valueOf();
if (!this.modelChanges[key]) {
this.modelChanges[key] = A();
}
let lastObject = this.modelChanges[key].get('lastObject');
if (Array.isArray(lastObject)) {
lastObject = JSON.stringify(lastObject);
}
if (!this.modelChanges[key].length) {
this.modelChanges[key].pushObject(rawValue);
} else {
if (lastObject === rawValue) {
return;
}
if (String(lastObject).valueOf() !== value) {
this.modelChanges[key].pushObject(rawValue);
}
}
log(key, this.modelChanges[key], lastObject, rawValue);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment