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 = '') { 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); } }