-
-
Save nightire/ee116816c5a45aa26d90f5d75830023c to your computer and use it in GitHub Desktop.
nested save
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Promise, defer } from 'rsvp'; | |
import { get } from '@ember/object'; | |
// a mock API response used below | |
const MOCK_PAYLOAD = { | |
person: { | |
id: '1', | |
name: 'Chris', | |
age: 33, | |
father: { | |
id: '2', | |
name: 'Tom', | |
age: 65 | |
}, | |
friends: [ | |
{ id: '3', name: 'Rebecca', age: 32 }, | |
{ id: '4', name: 'Wesley', age: 35 }, | |
] | |
} | |
} | |
// the guts of the operation | |
// note that for this to work the transaction needs | |
// to be coupled to the response format the API will | |
// utilize in how it builds its `path` for locating | |
// the id later. | |
class Transaction { | |
records = new Map(); | |
deferred = defer(); | |
get promise() { | |
return deferred.promise; | |
} | |
constructor(snapshot) { | |
this.records.set(snapshot.record, true); | |
this.recursivelySave(snapshot, snapshot.modelName); | |
} | |
async resolve(payload) { | |
let completed = []; | |
this.records.forEach((promises, record) => { | |
if (promises === true) { return } | |
const { path, type } = promises; | |
let data = get(payload, path); | |
// we locationally (by path) map the id within the response | |
// back to the original record. By making sure the original | |
// record is assigned this ID when the primary payload is | |
// processed we don't end up with duplicates or records in an | |
// unsaved state. | |
promises.resolver.resolve({ data: { | |
type, | |
id: `${data.id}` | |
} }); | |
completed.push(promises.final); | |
}); | |
await Promise.all(completed); | |
// add another delay for effect | |
await new Promise(resolve => setTimeout(resolve, 1500)); | |
return payload; | |
} | |
recursivelySave(snapshot, path = '<missing>') { | |
snapshot.eachRelationship((name, meta) => { | |
let related = snapshot[meta.kind](name); | |
if (related) { | |
if (Array.isArray(related)) { | |
related.forEach((r, index) => this.add(r, path + '.' + name + '.' + index)); | |
} else { | |
this.add(related, path + '.' + name); | |
} | |
} | |
}); | |
} | |
add(snapshot, path = '') { | |
// ignore this record if we're already saving it as part of the transaction | |
let record = snapshot.record; | |
if (this.records.has(record)) { | |
return false; | |
} | |
// initiate a save on the record | |
let resolver = defer(); | |
let save = record.save({ adapterOptions: { transaction: this } }); | |
let promises = { save, resolver, path }; | |
this.records.set(record, promises); | |
this.recursivelySave(snapshot, path); | |
return promises; | |
} | |
get(record) { | |
return this.records.get(record); | |
} | |
} | |
export default class AppAdapter { | |
static create() { | |
return new this(); | |
} | |
createRecord() { | |
return this.saveRecordAndChildren(...arguments); | |
} | |
// in a real app the difference here may be a POST vs PUT or PATCH | |
// but we don't care so we're just going to send create and update | |
// through the same save path. | |
updateRecord() { | |
return this.saveRecordAndChildren(...arguments); | |
} | |
// you could cascade deletes this way too | |
deleteRecord() {} | |
async saveRecordAndChildren(store, ModelClass, snapshot) { | |
let transaction = snapshot.adapterOptions?.transaction; | |
if (!transaction) { | |
// we are the primary (originating) save call | |
transaction = new Transaction(snapshot); | |
} else { | |
// we are one of the nested saves | |
let promises = transaction.get(snapshot.record); | |
return promises.resolver.promise; | |
} | |
// simulate getting back a payload after some time spent | |
// on network | |
await new Promise(resolve => setTimeout(resolve, 1500)); | |
return transaction.resolve(MOCK_PAYLOAD); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
export default Ember.Controller.extend({ | |
appName: 'Ember Twiddle', | |
init() { | |
this._super(...arguments); | |
let store = this.get('store'); | |
let user = store.createRecord('person', { name: 'Chris' }); | |
let father = store.createRecord('person', { name: 'Tom' }); | |
let friend1 = store.createRecord('person', { name: 'Rebecca' }); | |
let friend2 = store.createRecord('person', { name: 'Wesley' }); | |
let friends = user.friends; | |
friends.addObject(friend1); | |
friends.addObject(friend2); | |
user.father = father; | |
this.user = user; | |
// note if we only wanted to save specific related records | |
// and not all related records we could pass adapterOptions | |
// into this save with a list of which related props the transaction should include | |
// the transaction class here would need to be updated to then include only those vs the current recursive strategy | |
// the setTimeout here is just to add some delay so we | |
// can observe the states change | |
new Promise(resolve => setTimeout(resolve, 1500)) | |
.then(() => user.save()); | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | |
age: attr(), | |
father: belongsTo('person', { async: false, inverse: null }), | |
friends: hasMany('person', { async: false, inverse: 'friends' }) | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class MySerializer { | |
static create() { | |
return new this(); | |
} | |
normalizeResponse(_, __, rawData) { | |
if (rawData.data) { | |
return rawData; | |
} | |
const data = normalizePerson(rawData.person); | |
const included = []; | |
if (rawData.person.father) { | |
included.push(normalizePerson(rawData.person.father)); | |
} | |
if (rawData.person.friends) { | |
rawData.person.friends.forEach(f => { | |
included.push(normalizePerson(f)); | |
}); | |
} | |
return { data, included }; | |
} | |
} | |
function normalizePerson(person) { | |
const data = { | |
type: 'person', | |
id: `${person.id}`, | |
attributes: { | |
name: person.name, | |
age: person.age | |
}, | |
relationships: {} | |
}; | |
if (person.friends) { | |
data.relationships.friends = { | |
data: person.friends.map(r => { return { type: 'person', id: `${r.id}` }; }) | |
}; | |
} | |
if (person.father) { | |
data.relationships.father = { data: { type: 'person', id: `${person.father.id}` } } | |
} | |
return data; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": "0.15.1", | |
"EmberENV": { | |
"FEATURES": {} | |
}, | |
"options": { | |
"use_pods": false, | |
"enable-testing": false | |
}, | |
"dependencies": { | |
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js", | |
"ember": "3.18.1", | |
"ember-template-compiler": "3.18.1", | |
"ember-testing": "3.18.1" | |
}, | |
"addons": { | |
"ember-data": "3.4.2" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment