Skip to content

Instantly share code, notes, and snippets.

@borissuska
Last active July 20, 2017 13:55
Show Gist options
  • Save borissuska/d8e4d75708b6ea763537a1f40a9cf70c to your computer and use it in GitHub Desktop.
Save borissuska/d8e4d75708b6ea763537a1f40a9cf70c to your computer and use it in GitHub Desktop.
Check relationships change
import Adapter from "ember-data/adapters/json-api";
export default Adapter.extend();
import Ember from 'ember';
import Session from '../utils/session';
export default Ember.Controller.extend({
store: Ember.inject.service(),
model: undefined,
session: undefined,
modelToEdit: undefined,
allRelated: undefined,
actions: {
loadModel() {
this.get('store').findRecord('myModel', 1).then((model) => this.set('model', model));
},
editModel() {
let session = Session.create();
this.set('session', session);
this.get('store').findAll('myRelated').then((related) => this.set('allRelated', related));
session.add(this.get('model')).then((model) => {
this.set('modelToEdit', model)
});
},
changeRandomly() {
Ember.$.get('/my-models/1/random-change');
},
changeRelated(related, event) {
// let related = this.get('store').peekRecord('myRelated', relatedId);
if (event.target.checked) {
related.set('model', this.get('modelToEdit'));
// this.get('modelToEdit.related').addObject(related);
} else {
related.set('model', undefined);
// this.get('modelToEdit.related').removeObject(related);
}
},
submit() {
this.get('session').submit();
},
rollback() {
this.get('session').rollback();
this.set('session', undefined);
this.set('modelToEdit', undefined);
}
}
});
export function returnJSON(status, body) {
return json(...arguments);
};
export function json(status, body) {
if (arguments.length === 1) {
body = status;
status = 200;
}
return [
status,
{ "Content-Type": "application/json" },
JSON.stringify(body)
];
};
export const server = new Pretender();
export function initialize() {
server.handledRequest = function(verb, path, request) {
console.log(`handled request to ${verb} ${path}`);
};
server.unhandledRequest = function(verb, path, request) {
console.log(`undhandled request ${verb} ${path}`, JSON.parse(request.requestBody));
};
let db = {
myModel: {
1: {
attributes: {
name: 'model 1',
version: 1
},
relationships: {
related: {
data: [
{ type: 'my-related', id: '1' },
{ type: 'my-related', id: '2' },
{ type: 'my-related', id: '3' }
]
}
}
}
},
myRelated: {
1: {
attributes: {
name: 'related 1',
version: 1
},
relationships: {
model: {
data: { type: 'my-model', id: '1'}
}
}
},
2: {
attributes: {
name: 'related 2',
version: 1
},
relationships: {
model: {
data: { type: 'my-model', id: '1'}
}
}
},
3: {
attributes: {
name: 'related 3',
version: 1
},
relationships: {
model: {
data: { type: 'my-model', id: '1'}
}
}
},
4: {
attributes: {
name: 'related 4',
version: 1
},
relationships: {
model: {
data: null
}
}
},
5: {
attributes: {
name: 'related 5',
version: 1
},
relationships: {
model: {
data: null
}
}
}
}
};
function randomizeRelated(modelId, model = db['myModel'][modelId]) {
const a = [];
const relatedIds = Object.keys(db['myRelated']);
const rate = 1 + Math.floor(Math.random() * 5);
let lastIdx = 0;
for (let i=0; i < relatedIds.length; i++) {
let rId = relatedIds[i];
let refModel = db['myRelated'][rId].relationships.model;
// exclude approximatelly 1 of RATE items
if (Math.floor(Math.random() * 10000) % rate !== 0) {
a[lastIdx++] = { type: 'my-related', id: rId };
refModel.data = { type: 'my-model', id: modelId };
} else if (refModel.data && refModel.data.id === modelId) {
refModel.data = null;
}
}
model.relationships.related.data = a;
}
server.map(function() {
// Get latest version of my model
this.get('/my-models/:id', function(request) {
return json({
data: Object.assign({
type: 'my-model',
id: request.params.id
}, db['myModel'][request.params.id])
});
});
// Randomly change my model - it simulates concurrent update
this.get('/my-models/:id/random-change', function(request) {
const model = db['myModel'][request.params.id];
// remove random
model.attributes.name = model.attributes.name.replace(/\s\(rev\.\s\d+\)$/, '');
model.attributes.name += ` (rev. ${Math.round(Math.random()*10000)})`;
randomizeRelated(request.params.id, model);
model.attributes.version++;
return json({status: 'ok'});
});
// update my model only if version matches
this.patch('/my-models/:id', function(request) {
let originalData = db['myModel'][request.params.id];
let newData = JSON.parse(request.requestBody).data;
if (originalData.attributes.version === newData.attributes.version) {
newData.attributes.version++;
db[request.params.id] = newData;
return json(202, {
data: Object.assign({
type: 'my-model',
id: request.params.id
}, newData)
});
} else {
return json(409, {
data: Object.assign({
type: 'my-model',
id: request.params.id,
}, newData),
errors: [{
code: 409,
status: "409",
title: "Concurrent modification of entity",
source: {
pointer: "data/attributes/version"
}
}]
});
}
});
// Get latest version of my-related
this.get('/my-relateds', function(request) {
let relatedIds = Object.keys(db['myRelated']);
let data = [];
for (let i=0; i < relatedIds.length; i++) {
let rId = relatedIds[i]
data.push(Object.assign({
type: 'my-related',
id: rId
}, db['myRelated'][rId])
);
}
return json({
data: data
});
});
this.get('/my-relateds/:id', function(request) {
return json({
data: Object.assign({
type: 'my-related',
id: request.params.id
}, db['myRelated'][request.params.id])
});
});
});
};
export default {
name: 'pretender',
initialize
};
import Model from "ember-data/model";
import attr from "ember-data/attr";
import { belongsTo, hasMany } from "ember-data/relationships";
export default Model.extend({
name: attr('string'),
version: attr('number'),
related: hasMany('myRelated', { async: true, inverse: 'model' })
});
.clearfix {
margin: 0 -15px;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.clearfix { display: inline-block; }
/* start commented backslash hack \*/
* html .clearfix { height: 1%; }
.clearfix { display: block; }
/* close commented backslash hack */
.half {
width: 50%;
float: left;
padding: 0 15px;
}
.rest {
width: auto;
overflow: hidden;
}
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
<h1>Relatioship change</h1>
<br>
<br>
<div class="clearfix">
<div class="half">
Id: {{model.id}}<br>
Name: {{model.name}}<br>
Version: {{model.version}}<br>
Has dirty attributes: {{model.hasDirtyAttributes}}<br>
<ul>
{{#each model.related as |rel|}}
<li>{{rel.name}}</li>
{{/each}}
</ul>
</div>
<div class="rest">
{{#if modelToEdit}}
<form>
Id: {{modelToEdit.id}}<br>
Name: {{input value=modelToEdit.name}}<br>
Version {{modelToEdit.version}}<br>
Has dirty attributes: {{modelToEdit.hasDirtyAttributes}}<br>
<ul>
{{#each allRelated as |rel|}}
<li>{{input
type = "checkbox"
checked = (array-contains modelToEdit.related rel)
change = (action "changeRelated" rel)
}}{{rel.name}} /{{rel.id}}/ {{rel.model.name}} [{{rel.hasDirtyAttributes}}]
</li>
{{/each}}
</ul>
<button {{action "submit"}}>Submit</button>
<button {{action "rollback"}}>Rollback</button>
</form>
{{else}}
{{#if model}}
<button {{action "editModel"}}>Edit this model</button>
{{/if}}
{{/if}}
</div>
</div>
<br>
<button {{action "loadModel"}}>Load model</button>
<button {{action "changeRandomly"}}>Simulate concurrent change</button>
{
"version": "0.12.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.12.2",
"ember-template-compiler": "2.12.0",
"ember-testing": "2.12.0",
"route-recognizer": "https://rawgit.com/tildeio/route-recognizer/56f5fcec6ae58d8e86b5dc77609809fb91198142/dist/route-recognizer.js",
"FakeXMLHttpRequest": "https://rawgit.com/pretenderjs/FakeXMLHttpRequest/23c3a96b5b24f1bfe595867437e4f128a29c2840/fake_xml_http_request.js",
"pretender": "https://rawgit.com/pretenderjs/pretender/c6de9afe18b1472aded2592f5a80ad9a26a0e262/pretender.js"
},
"addons": {
"ember-data": "2.14.2",
"ember-array-contains-helper": "1.3.2",
"ember-concurrency": "0.8.7"
}
}
import Ember from 'ember';
import { task, all } from 'ember-concurrency';
const COPIES = "__session_copies__";
const COPY_TASK_RUNNER = "__session_copy_task_runner__";
const COPY_TASK = "__session_copy_task__";
const {
assign,
inject,
Logger,
guidFor,
isEmpty,
runInDebug,
getOwner
} = Ember;
const {
keys
} = Object;
const PRIMITIVE_TYPES = ['string', 'number', 'boolean'];
export default Ember.Object.extend({
[COPIES]: Ember.computed(function() { return {}; }),
_transforms: Ember.computed(function() { return {}; }),
/**
* Get the transform for a given type.
*
* @method getTransform
* @private
* @param {DS.Model} model
* @param {String} type
* @return {DS.Transform}
*/
getTransform(model, type) {
let transform = this.get(`_transforms.${type}`);
if (!transform) {
transform = getOwner(model).lookup(`transform:${type}`);
this.set(`_transforms.${type}`, transform);
}
return transform;
},
/**
* The copy task runner. Allows our copy task to have a drop
* concurrency policy
*
* @type {Task}
* @private
*/
[COPY_TASK_RUNNER]: task(function *(model) {
let isSuccessful = false;
try {
let copy = yield this.get(COPY_TASK).perform(model);
isSuccessful = true;
return copy;
} catch (e) {
runInDebug(() => Logger.error('[ember-data-session]', e));
// Throw so the task promise will be rejected
throw new Error(e);
} finally {
if (!isSuccessful) {
let copiesKeys = this.rollback();
// Display the error
runInDebug(() => Logger.error(`[ember-data-session] Failed to copy model '${model}'. Cleaning up ${copiesKeys.length} created copies...`));
}
}
}).drop(),
/**
* The copy task that gets called from `copy`. Does all the grunt work.
*
* NOTE: This task cannot have a concurrency policy since it breaks cyclical
* relationships.
*
* @type {Task}
* @private
*/
[COPY_TASK]: task(function *(model) {
let { modelName } = model.constructor;
let copies = this.get(COPIES);
let store = getOwner(model).lookup('service:store');
let guid = guidFor(model);
let attrs = {};
// Handle cyclic relationships: If the model has already been copied,
// just return that model
if (copies[guid]) {
console.log('copy-guid', copies[guid] + '');
return copies[guid];
}
let copy = store.createRecord(modelName);
copies[guid] = copy;
// Copy all the attributes
model.eachAttribute((name, { type, options }) => {
if (!isEmpty(type) && !PRIMITIVE_TYPES.includes(type)) {
let value = model.get(name);
let transform = getTransform(model, type);
// Run the transform on the value. This should guarantee that we get
// a new instance.
value = transform.serialize(value, options);
value = transform.deserialize(value, options);
attrs[name] = value;
} else {
attrs[name] = model.get(name);
}
});
let relationships = [];
// Get all the relationship data
model.eachRelationship((name, meta) => {
relationships.push({ name, meta });
});
// Copy all the relationships
for (let i = 0; i < relationships.length; i++) {
let { name, meta } = relationships[i];
let value = yield model.get(name);
if (meta.kind === 'belongsTo') {
if (value) {
attrs[name] = yield this.get(COPY_TASK).perform(value);
} else {
attrs[name] = value;
}
} else if (meta.kind === 'hasMany') {
let allRels = [];
value.forEach((rel) => allRels.push(this.get(COPY_TASK).perform(rel)));
attrs[name] = yield all(allRels);
}
}
// Set the properties on the copied model
copy.setProperties(attrs);
return copy;
}),
add(model) {
// return copy instance
return this.get(COPY_TASK_RUNNER).perform(model);
},
rollback() {
let store = undefined;
let copies = this.get(COPIES);
let copiesKeys = keys(copies);
// Unload all created records
copiesKeys.forEach((key) => {
let copy = copies[key];
if (!store) {
store = getOwner(copy).lookup('service:store');
}
store.unloadRecord(copy);
});
this.set(COPIES, {});
return copiesKeys;
},
submit() {
let copies = this.get(COPIES);
let copiesKeys = keys(copies);
let copiesPromisses = [];
// save all records
copiesKeys.forEach((key) => {
copiesPromisses.push(copies[key].save());
});
return Ember.RSVP.all(copiesPromisses);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment