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); } });