Skip to content

Instantly share code, notes, and snippets.

@runspired
Created October 5, 2018 16:22
Show Gist options
  • Save runspired/a607f4debabde043efd284a04b244974 to your computer and use it in GitHub Desktop.
Save runspired/a607f4debabde043efd284a04b244974 to your computer and use it in GitHub Desktop.
Ember Data | Save Transaction
import Adapter from 'ember-data/adapters/json-api';
import Ember from 'ember';
import RSVP from 'rsvp';
const { Promise } = RSVP;
const { get } = Ember;
class Transaction {
constructor(store, ModelClass, primary, transactionMembers) {
this.store = store;
this.primary = primary;
this.members = [];
let relsByName = get(ModelClass, 'relationshipsByName');
let transaction = this;
transactionMembers.forEach((name) => {
let meta = relsByName.get(name);
if (!meta) {
throw new Error(`Unknown relationship ${name} was specified as a transactionMember while saving ${snapshot.record}`);
}
if (meta.kind === 'belongsTo') {
transaction.add({
key:name,
kind: meta.kind,
data: primary.belongsTo(name)
});
} else if (meta.kind === 'hasMany') {
let snapshots = primary.hasMany(name);
snapshots.forEach((snapshot, index) => {
transaction.add({
key: name,
kind: meta.kind,
index,
data: snapshot
})
});
}
});
}
add(member) {
let promise = new Promise(resolve => {
member.resolve = resolve;
});
member.promise = promise;
return this.members.push(member);
}
begin() {
this.members.forEach(r => {
r.data.record.save({
adapterOptions: {
transactionPromise: r.promise
}
});
});
}
complete(jsonApiDocument) {
this.members.forEach(member => {
this.extractMemberFromPayload(member, jsonApiDocument);
});
}
extractMemberFromPayload(member, doc) {
let dataPath = `data.relationships.${member.key}.data`;
let data = get(doc, dataPath);
let ref;
if (!doc.included) {
throw new Error('No includes!');
}
if (!data) {
throw new Error('No relationship payload!');
}
if (member.kind === 'belongsTo') {
ref = data;
} else {
if (!Array.isArray(data)) {
throw new Error('Expected array!');
}
ref = data[member.index];
}
let resolvePayload = doc.included.find(r => r.id === ref.id && r.type === ref.type);
member.resolve({ data: resolvePayload });
}
}
export default Adapter.extend({
createRecord(store, ModelClass, snapshot) {
let options = snapshot.adapterOptions;
if (options && Array.isArray(options.transactionMembers) && !options.transactionInitialized) {
return this.batchCreate(store, ModelClass, snapshot);
} else if (options && options.transactionPromise) {
return options.transactionPromise;
}
// super here in real life and ensure that
// serializer.serializeIntoHash correctly adds relationships
// return this._super(store, ModelClass, snapshot);
return Promise.resolve(makeCreateResponse(snapshot));
},
batchCreate(store, ModelClass, snapshot) {
let options = snapshot.adapterOptions;
let { transactionMembers } = options;
options.transactionInitialized = true;
let transaction = new Transaction(store, ModelClass, snapshot, transactionMembers);
transaction.begin();
let promise = this.createRecord(store, ModelClass, snapshot);
return promise.then((rawPayload) => {
let serializer = store.serializerFor(ModelClass.modelName);
// short circuit for the example
let normalizedResponse = rawPayload;
// what you would do in real life
// let normalizedResponse = serializer.normalizeResponse(store, ModelClass, rawPayload, null, 'createRecord');
transaction.complete(normalizedResponse);
// TODO this is trolly, but rawPayload can't be trusted either
return normalizedResponse;
});
}
});
let fakeId = 1;
function makeCreateResponse(snapshot) {
let response = {};
let data = response.data = {};
let included = response.included = [];
data.type = snapshot.modelName;
data.id = `fake-server-id-${fakeId++}`;
data.attributes = snapshot.attributes();
let relationships = data.relationships = {};
let includes = snapshot.adapterOptions.transactionMembers;
let rels = get(snapshot.type, 'relationshipsByName');
includes.forEach(name => {
let meta = rels.get(name);
if (meta.kind === 'belongsTo') {
// to belongsTo things
} else {
let relationship = relationships[name] = {};
let rData = relationship.data = [];
snapshot.hasMany(name).forEach((s, index) => {
let type = s.modelName;
let id = `fake-server-id-${fakeId++}`;
rData.push({ type, id });
included.push({
type,
id,
attributes: s.attributes()
});
});
}
});
return response;
}
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
actions: {
createParent(name) {
if (!name) { return }
let store = this.get('store');
let person = store.createRecord('person', { name });
this.set('parent', person);
this.set('newPersonName', '');
},
createChild(name) {
if (!name) { return }
let store = this.get('store');
let parent = this.get('parent');
let person = store.createRecord('person', { name, parent });
this.set('newPersonName', '');
},
save() {
this.get('parent').save({
adapterOptions: {
transactionMembers: ['children']
}
});
}
}
});
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(),
parent: belongsTo('person', { async: false, inverse: 'children' }),
children: hasMany('person', { async: false, inverse: 'parent' })
});
import Serializer from 'ember-data/serializers/json-api';
export default Serializer.extend();
<h3>Saving Multiple Records via a single Transaction in EmberData</h3>
<p>A simple example</p>
<br>
{{#if parent}}
Parent: {{parent.name}} - {{parent.id}} {{if parent.isNew 'unsaved' 'save'}}<br>
<ul>
{{#each parent.children as |child|}}
<li>{{child.name}} - {{child.id}} {{if child.isNew 'unsaved' 'save'}}</li>
{{else}}
{{parent.name}} has no children. Add some!
{{/each}}
</ul>
{{input type="text" value=newPersonName}}
<button {{action "createChild" newPersonName}}>Add Child</button>
{{#if parent.children.length}}
<button {{action "save"}}>Save</button>
{{/if}}
{{else}}
{{input type="text" value=newPersonName}}
<button {{action "createParent" newPersonName}}>Create</button>
{{/if}}
<br>
{{outlet}}
<br>
<br>
{
"version": "0.13.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.16.2",
"ember-template-compiler": "2.16.2",
"ember-testing": "2.16.2"
},
"addons": {
"ember-data": "2.16.3"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment