eflex-ember-data-copyable
Version:
Intelligently copy an Ember Data model and all of its relationships
252 lines (215 loc) • 7.25 kB
JavaScript
import getTransform from 'ember-data-copyable/utils/get-transform';
import isUndefined from 'ember-data-copyable/utils/is-undefined';
import {
COPY_TASK,
COPY_TASK_RUNNER,
IS_COPYABLE
} from 'ember-data-copyable/-private/symbols';
import { task, all } from 'ember-concurrency';
import { assign } from '@ember/polyfills';
import { guidFor } from '@ember/object/internals';
import { Copyable } from 'ember-copy';
import { isEmpty } from '@ember/utils';
import { runInDebug } from '@ember/debug';
import Mixin from '@ember/object/mixin';
const { keys } = Object;
const PRIMITIVE_TYPES = ['string', 'number', 'boolean'];
const DEFAULT_OPTIONS = {
// List of all attributes to ignore
ignoreAttributes: [],
// List of other attributes to copy
otherAttributes: [],
// List of all attributes to copy by reference
copyByReference: [],
// Overwrite specific keys with a given value
overwrite: {},
// Relationship options
relationships: {}
};
export default Mixin.create({
/**
* Copyable options for the specific model. See DEFAULT_OPTIONS for details
*
* @type {Object}
* @public
*/
copyableOptions: null,
/**
* @type {Boolean}
* @private
*/
[IS_COPYABLE]: true,
/**
* Entry point for copying the model
*
* @method copy
* @public
* @async
* @param {Boolean} deep If `true`, a deep copy of the model will be made
* @param {Object} options Options for the copy which will override model
* specified options. See DEFAULT_OPTIONS.
* @return {TaskInstance} A promise like TaskInstance
*/
copy(/* deep, options */) {
return this.get(COPY_TASK_RUNNER).perform(...arguments);
},
/**
* The copy task runner. Allows our copy task to have a drop
* concurrency policy
*
* @type {Task}
* @private
*/
[COPY_TASK_RUNNER]: task(function*(deep, options) {
const _meta = { copies: {}, transforms: {} };
const store = this.get('store');
let isSuccessful = false;
try {
const model = yield this.get(COPY_TASK).perform(deep, options, _meta);
isSuccessful = true;
return model;
} catch (e) {
// eslint-disable-next-line no-console
runInDebug(() => console.error('[ember-data-copyable]', e));
// Throw so the task promise will be rejected
throw new Error(e);
} finally {
if (!isSuccessful) {
const copiesKeys = keys(_meta.copies);
// Display the error
runInDebug(() =>
// eslint-disable-next-line no-console
console.error(
`[ember-data-copyable] Failed to copy model '${this}'. Cleaning up ${
copiesKeys.length
} created copies...`
)
);
// Unload all created records
copiesKeys.forEach(key => store.unloadRecord(_meta.copies[key]));
}
}
}).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*(deep, options, _meta) {
options = assign({}, DEFAULT_OPTIONS, this.get('copyableOptions'), options);
const {
ignoreAttributes,
otherAttributes,
copyByReference,
overwrite
} = options;
const { copies } = _meta;
const { modelName } = this.constructor;
const store = this.get('store');
const guid = guidFor(this);
const relationships = [];
let attrs = {};
// Handle cyclic relationships: If the model has already been copied,
// just return that model
if (copies[guid]) {
return copies[guid];
}
const model = store.createRecord(modelName);
copies[guid] = model;
// Copy all the attributes
this.eachAttribute((name, { type, isFragment, options: attributeOptions }) => {
if (ignoreAttributes.includes(name)) {
return;
} else if (!isUndefined(overwrite[name])) {
attrs[name] = overwrite[name];
} else if (
!isEmpty(type) &&
!copyByReference.includes(name) &&
!PRIMITIVE_TYPES.includes(type)
) {
let value = this.get(name);
if ((Copyable && Copyable.detect(value)) || (value && isFragment)) {
// "value" is an Ember.Object using the ember-copy addon
// (ie. old deprecated Ember.Copyable API - if you use
// the "Ember Data Model Fragments" addon and "value" is a fragment or
// if use your own serializer where you deserialize a value to an
// Ember.Object using this Ember.Copyable API)
value = value.copy(deep);
} else if (!isFragment) {
const transform = getTransform(this, type, _meta);
// Run the transform on the value. This should guarantee that we get
// a new instance.
value = transform.serialize(value, attributeOptions);
value = transform.deserialize(value, attributeOptions);
}
attrs[name] = value;
} else {
attrs[name] = this.get(name);
}
});
// Get all the relationship data
this.eachRelationship((name, meta) => {
if (!ignoreAttributes.includes(name)) {
relationships.push({ name, meta });
}
});
// Copy all the relationships
for (let i = 0; i < relationships.length; i++) {
const { name, meta } = relationships[i];
if (!isUndefined(overwrite[name])) {
attrs[name] = overwrite[name];
continue;
}
// We dont need to yield for a value if it's just copied by ref
// or if we are doing a shallow copy
if (!deep || copyByReference.includes(name)) {
try {
const ref = this[meta.kind](name);
const copyRef = model[meta.kind](name);
copyRef[`${meta.kind}Relationship`].addRecordDatas(
ref[`${meta.kind}Relationship`].members
);
} catch (e) {
attrs[name] = yield this.get(name);
}
continue;
}
const value = yield this.get(name);
const relOptions = options.relationships[name];
const deepRel =
relOptions && typeof relOptions.deep === 'boolean'
? relOptions.deep
: deep;
if (meta.kind === 'belongsTo') {
if (value && value.get(IS_COPYABLE)) {
attrs[name] = yield value
.get(COPY_TASK)
.perform(deepRel, relOptions, _meta);
} else {
attrs[name] = value;
}
} else if (meta.kind === 'hasMany') {
const firstObject = value.get('firstObject');
if (firstObject && firstObject.get(IS_COPYABLE)) {
attrs[name] = yield all(
value
.getEach(COPY_TASK)
.invoke('perform', deepRel, relOptions, _meta)
);
} else {
attrs[name] = value;
}
}
}
// Build the final attrs pojo by merging otherAttributes, the copied
// attributes, and ant overwrites specified.
attrs = assign(this.getProperties(otherAttributes), attrs, overwrite);
// Set the properties on the model
model.setProperties(attrs);
return model;
})
});