ember-data-factory-guy
Version:
Factories for testing Ember applications using EmberData
446 lines (388 loc) • 13.7 kB
JavaScript
import { assert } from '@ember/debug';
import { entries } from '../utils/helper-functions';
import { typeOf } from '@ember/utils';
import { getOwner } from '@ember/application';
import { camelize } from '@ember/string';
import {
macroCondition,
dependencySatisfies,
importSync,
} from '@embroider/macros';
let Fragment;
let FragmentArray;
if (macroCondition(dependencySatisfies('ember-data-model-fragments', '*'))) {
Fragment = importSync('ember-data-model-fragments/fragment').default;
FragmentArray = importSync(
'ember-data-model-fragments/array/fragment'
).default;
}
/**
Base class for converting the base fixture that factory guy creates to
the payload expected by ember data adapter.
While it's converting the format, it transforms the attribute and relationship keys.
Don't want to transform keys when building payload for a FactoryGuy#make/makeList operation,
but only for build/buildList.
serializerMode (true) means => produce json as if ember data was serializing the payload
to go back to the server.
So, what does serializerMode ( false ) mean? produce json that can immediately be consumed
by ember data .. but it is such a long story .. that I will have to explain another time
If there are associations in the base fixture, they will be added to the
new fixture as 'side loaded' elements, even if they are another json payload
built with the build/buildList methods.
@param {DS.Store} store
@param {Object} options
transformKeys transform keys and values in fixture if true
serializeMode act like serialization is for a return to server if true
@constructor
*/
export default class FixtureConverter {
constructor(store, { transformKeys = true, serializeMode = false } = {}) {
this.transformKeys = transformKeys;
this.serializeMode = serializeMode;
this.store = store;
this.listType = false;
this.noTransformFn = (x) => x;
this.defaultValueTransformFn = this.noTransformFn;
}
/**
Convert an initial fixture into a final payload.
This raw fixture can contain other json in relationships that were
built by FactoryGuy ( build, buildList ) methods
@param modelName
@param fixture
@returns {*} converted fixture
*/
convert(modelName, fixture) {
let data;
if (typeOf(fixture) === 'array') {
this.listType = true;
data = fixture.map((single) => {
return this.convertSingle(modelName, single);
});
} else {
data = this.convertSingle(modelName, fixture);
}
let payload = this.createPayload(modelName, data);
this.addIncludedArray(payload);
return payload;
}
/**
Empty response is a special case, so use this method for generating it.
@param _
@param {Object} options useValue to override the null value that is passed
@returns {Array|null}
*/
emptyResponse(_, options = {}) {
return options.useValue || null;
}
/**
* Use the primaryKey from the serializer if it is declared
*
* @param modelName
* @param data
* @param fixture
*/
addPrimaryKey(modelName, data, fixture) {
let primaryKey = this.store.serializerFor(modelName).get('primaryKey'),
primaryKeyValue = fixture[primaryKey] || fixture.id;
// model fragments will have no primaryKey and don't want them to have id
if (primaryKeyValue) {
// need to set the id for all as a baseline
data.id = primaryKeyValue;
// if the id is NOT the primary key, need to make sure that the primaryKey
// has the primaryKey value
data[primaryKey] = primaryKeyValue;
}
}
transformRelationshipKey(relationship) {
let { parentModelName, parentType } = relationship,
type =
parentModelName ||
(parentType && parentType.modelName) ||
relationship.type,
transformFn = this.getTransformKeyFunction(type, 'Relationship');
return transformFn(relationship.name, relationship.kind);
}
getRelationshipType(relationship, fixture) {
let isPolymorphic = relationship.options.polymorphic;
return isPolymorphic
? this.polymorphicTypeTransformFn(fixture.type)
: relationship.type;
}
attributeIncluded(attribute, modelName) {
if (!this.serializeMode) {
return true;
}
let serializer = this.store.serializerFor(modelName),
attrOptions = this.attrsOption(serializer, attribute);
if (attrOptions && attrOptions.serialize === false) {
return false;
}
return true;
}
getTransformKeyFunction(modelName, type) {
if (!this.transformKeys) {
return this.noTransformFn;
}
let serializer = this.store.serializerFor(modelName),
keyFn = serializer['keyFor' + type] || this.defaultKeyTransformFn;
return (attribute, method) => {
// if there is an attrs override in serializer, return that first
let attrOptions = this.attrsOption(serializer, attribute),
attrName;
if (attrOptions) {
if (attrOptions.key) {
attrName = attrOptions.key;
}
if (typeOf(attrOptions) === 'string') {
attrName = attrOptions;
}
}
return (
attrName || keyFn.apply(serializer, [attribute, method, 'serialize'])
);
};
}
getTransformValueFunction(type) {
if (!this.transformKeys || (type && type.match('-mf'))) {
return this.noTransformFn;
}
if (!type) {
return this.defaultValueTransformFn;
}
let container = getOwner(this.store),
transform = container.lookup('transform:' + type);
assert(
`[ember-data-factory-guy] could not find
the [ ${type} ] transform. If you are in a unit test, be sure
to include it in the list of needs as [ transform:${type} ], Or set your
unit test to be [ integration: true ] and include everything.`,
transform
);
let transformer = container.lookup('transform:' + type);
return transformer.serialize.bind(transformer);
}
extractAttributes(modelName, fixture) {
let attributes = {},
transformKeyFunction = this.getTransformKeyFunction(
modelName,
'Attribute'
);
this.store.modelFor(modelName).eachAttribute((attribute, meta) => {
if (this.attributeIncluded(attribute, modelName)) {
let attributeKey = transformKeyFunction(attribute),
transformValueFunction = this.getTransformValueFunction(meta.type);
let attributeValueInFixture = fixture[attributeKey];
// If passed Fragments or FragmentArrays we must transform them to their serialized form before we can push them into the Store
if (
(Fragment && attributeValueInFixture instanceof Fragment) ||
(FragmentArray && attributeValueInFixture instanceof FragmentArray)
) {
fixture[attributeKey] = this.normalizeModelFragments(
attributeValueInFixture
);
}
if (Object.prototype.hasOwnProperty.call(fixture, attribute)) {
attributes[attributeKey] = transformValueFunction(
fixture[attribute],
meta.options
);
} else if (
Object.prototype.hasOwnProperty.call(fixture, attributeKey)
) {
attributes[attributeKey] = transformValueFunction(
fixture[attributeKey],
meta.options
);
}
}
});
return attributes;
}
normalizeModelFragments(attributeValueInFixture) {
if (Fragment && attributeValueInFixture instanceof Fragment) {
return this.store.normalize(
attributeValueInFixture.constructor.modelName,
attributeValueInFixture.serialize()
).data.attributes;
}
if (FragmentArray && attributeValueInFixture instanceof FragmentArray) {
return attributeValueInFixture
.serialize()
.map(
(item) =>
this.store.normalize(attributeValueInFixture.type, item).data
.attributes
);
}
}
/**
Extract relationships and descend into those looking for others
@param {String} modelName
@param {Object} fixture
@returns {{}}
*/
extractRelationships(modelName, fixture) {
let relationships = {};
this.store.modelFor(modelName).eachRelationship((key, relationship) => {
if (Object.prototype.hasOwnProperty.call(fixture, key)) {
if (relationship.kind === 'belongsTo') {
this.extractBelongsTo(
fixture,
relationship,
modelName,
relationships
);
} else if (relationship.kind === 'hasMany') {
this.extractHasMany(fixture, relationship, modelName, relationships);
}
}
});
return relationships;
}
/**
Extract belongTo relationships
@param fixture
@param relationship
@param relationships
*/
extractBelongsTo(fixture, relationship, parentModelName, relationships) {
let belongsToRecord = fixture[relationship.name],
isEmbedded = this.isEmbeddedRelationship(
parentModelName,
relationship.name
),
relationshipKey = isEmbedded
? relationship.name
: this.transformRelationshipKey(relationship);
let data = this.extractSingleRecord(
belongsToRecord,
relationship,
isEmbedded
);
relationships[relationshipKey] = this.assignRelationship(data);
}
// Borrowed from ember data
// checks config for attrs option to embedded (always)
isEmbeddedRelationship(modelName, attr) {
let serializer = this.store.serializerFor(modelName),
option = this.attrsOption(serializer, attr);
return (
option &&
(option.embedded === 'always' || option.deserialize === 'records')
);
}
attrsOption(serializer, attr) {
let attrs = serializer.get('attrs'),
option = attrs && (attrs[camelize(attr)] || attrs[attr]);
return option;
}
extractHasMany(fixture, relationship, parentModelName, relationships) {
let hasManyRecords = fixture[relationship.name],
relationshipKey = this.transformRelationshipKey(relationship),
isEmbedded = this.isEmbeddedRelationship(
parentModelName,
relationship.name
);
if (hasManyRecords && hasManyRecords.isProxy) {
return this.addListProxyData(
hasManyRecords,
relationship,
relationships,
isEmbedded
);
}
if (typeOf(hasManyRecords) !== 'array') {
return;
}
let records = hasManyRecords.map((hasManyRecord) => {
return this.extractSingleRecord(hasManyRecord, relationship, isEmbedded);
});
relationships[relationshipKey] = this.assignRelationship(records);
}
extractSingleRecord(record, relationship, isEmbedded) {
let data;
switch (typeOf(record)) {
case 'object':
if (record.isProxy) {
data = this.addProxyData(record, relationship, isEmbedded);
} else {
data = this.addData(record, relationship, isEmbedded);
}
break;
case 'instance':
data = this.normalizeAssociation(record, relationship);
break;
case 'number':
case 'string':
assert(
`Polymorphic relationships cannot be specified by id you
need to supply an object with id and type`,
!relationship.options.polymorphic
);
record = { id: record, type: relationship.type };
data = this.normalizeAssociation(record, relationship);
}
return data;
}
assignRelationship(object) {
return object;
}
/**
* Technically don't have to verify the links because hey would not even be assigned,
* but the user might want to know why
*
* @param modelName
* @param links
*/
verifyLinks(modelName, links = {}) {
const modelClass = this.store.modelFor(modelName),
relationships = modelClass.relationshipsByName;
for (let [relationshipKey, link] of entries(links)) {
assert(
`You defined a link url ${link} for the [${relationshipKey}] relationship
on model [${modelName}] but that relationship does not exist`,
relationships.get(relationshipKey)
);
}
}
addData(embeddedFixture, relationship, isEmbedded) {
let relationshipType = this.getRelationshipType(
relationship,
embeddedFixture
),
// find possibly more embedded fixtures
data = this.convertSingle(relationshipType, embeddedFixture);
if (isEmbedded) {
return data;
}
this.addToIncluded(data, relationshipType);
return this.normalizeAssociation(data, relationship);
}
// proxy data is data that was build with FactoryGuy.build method
addProxyData(jsonProxy, relationship, isEmbedded) {
let data = jsonProxy.getModelPayload(),
relationshipType = this.getRelationshipType(relationship, data);
if (isEmbedded) {
this.addToIncludedFromProxy(jsonProxy);
return data;
}
this.addToIncluded(data, relationshipType);
this.addToIncludedFromProxy(jsonProxy);
return this.normalizeAssociation(data, relationship);
}
// listProxy data is data that was build with FactoryGuy.buildList method
addListProxyData(jsonProxy, relationship, relationships, isEmbedded) {
let relationshipKey = this.transformRelationshipKey(relationship);
let records = jsonProxy.getModelPayload().map((data) => {
if (isEmbedded) {
return data;
}
let relationshipType = this.getRelationshipType(relationship, data);
this.addToIncluded(data, relationshipType);
return this.normalizeAssociation(data, relationship);
});
this.addToIncludedFromProxy(jsonProxy);
relationships[relationshipKey] = this.assignRelationship(records);
}
}