ember-data-factory-guy
Version:
Factories for testing Ember applications using EmberData
1,765 lines (1,665 loc) • 83 kB
JavaScript
import Store from '@ember-data/store';
import { assert } from '@ember/debug';
import { typeOf, isEmpty, isPresent } from '@ember/utils';
import { A } from '@ember/array';
import { macroCondition, dependencySatisfies, importSync, getOwnConfig } from '@embroider/macros';
import JSONSerializer from '@ember-data/serializer/json';
import RESTSerializer from '@ember-data/serializer/rest';
import JSONAPISerializer from '@ember-data/serializer/json-api';
import { camelize, dasherize, w, underscore } from '@ember/string';
import { getOwner } from '@ember/application';
import { pluralize } from 'ember-inflector';
import Pretender from 'pretender';
const plusRegex = new RegExp('\\+', 'g');
function paramsFromRequestBody(body) {
let params = {};
if (typeof body === 'string') {
if (body.match(/=/)) {
body = decodeURIComponent(body).replace(plusRegex, ' ');
(body.split('&') || []).map(param => {
const [key, value] = param.split('=');
params[key] = value;
});
} else if (body.match(/:/)) {
params = JSON.parse(body);
}
return params;
}
return body;
}
function toParams(obj) {
return parseParms(decodeURIComponent(param(obj)));
}
/**
* Iterator for object key, values
*
* @public
* @param obj
*/
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
function param(obj, prefix) {
let str = [],
p;
for (p in obj) {
if (Object.prototype.hasOwnProperty.call(obj, p)) {
var k = prefix ? prefix + '[' + p + ']' : p,
v = obj[p];
str.push(v !== null && typeof v === 'object' ? param(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(v));
}
}
return str.join('&');
}
function parseParms(str) {
let pieces = str.split('&'),
data = {},
i,
parts,
key,
value;
// Process each query pair
for (i = 0; i < pieces.length; i++) {
parts = pieces[i].split('=');
// No value, only key
if (parts.length < 2) {
parts.push('');
}
key = decodeURIComponent(parts[0]);
value = decodeURIComponent(parts[1]);
// Key is an array
if (key.indexOf('[]') !== -1) {
key = key.substring(0, key.indexOf('[]'));
// Check already there
if ('undefined' === typeof data[key]) {
data[key] = [];
}
data[key].push(value);
} else {
data[key] = value;
}
}
return data;
}
function isEmptyObject(obj) {
return !isObject(obj) || Object.keys(obj).length === 0;
}
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
function mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, {
[key]: {}
});
}
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
}
}
}
return mergeDeep(target, ...sources);
}
function isEquivalent(a, b) {
var type = typeOf(a);
if (type !== typeOf(b)) {
return false;
}
switch (type) {
case 'object':
return objectIsEquivalent(a, b);
case 'array':
return arrayIsEquivalent(a, b);
default:
return a === b;
}
}
function isPartOf(object, part) {
return Object.keys(part).every(function (key) {
return isEquivalent(object[key], part[key]);
});
}
function arrayIsEquivalent(arrayA, arrayB) {
if (arrayA.length !== arrayB.length) {
return false;
}
return arrayA.every(function (item, index) {
return isEquivalent(item, arrayB[index]);
});
}
function objectIsEquivalent(objectA, objectB) {
let aProps = Object.keys(objectA),
bProps = Object.keys(objectB);
if (aProps.length !== bProps.length) {
return false;
}
for (let i = 0; i < aProps.length; i++) {
let propName = aProps[i],
aEntry = objectA[propName],
bEntry = objectB[propName];
if (!isEquivalent(aEntry, bEntry)) {
return false;
}
}
return true;
}
/**
* Used to split a url with query parms into url and queryParams
* MockLinks and Mock both use this
*
* @param url
* @returns {*[]}
*/
function parseUrl(url) {
const [urlPart, query] = (url || '').split('?');
const params = query && query.split('&').reduce((params, param) => {
let [key, value] = param.split('=');
params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
return params;
}, {}) || {};
return [urlPart, params];
}
/**
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
*/
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, parentModelName) {
const type = /* parentModelName and parentType are not present on relationship schema in ember-data 5+, but these are here for backwards compat */
relationship.parentModelName || relationship.parentType?.modelName || /**/
parentModelName || 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) {
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);
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;
}
/**
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, parentModelName);
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, parentModelName),
isEmbedded = this.isEmbeddedRelationship(parentModelName, relationship.name);
if (hasManyRecords && hasManyRecords.isProxy) {
return this.addListProxyData(hasManyRecords, relationship, relationships, isEmbedded, parentModelName);
}
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, parentModelName) {
let relationshipKey = this.transformRelationshipKey(relationship, parentModelName);
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);
}
}
/**
* Using `serializeMode` to create a payload the way ember-data would serialize types
* when returning a payload to a server that accepts JSON-API wherin the types are
* pluralized
*
*/
class JSONAPIFixtureConverter extends FixtureConverter {
constructor(store, {
transformKeys = true,
serializeMode = false
} = {}) {
super(store, {
transformKeys,
serializeMode
});
this.typeTransformFn = this.serializeMode ? this.typeTransformViaSerializer : dasherize;
this.defaultKeyTransformFn = dasherize;
this.polymorphicTypeTransformFn = dasherize;
this.included = [];
}
typeTransformViaSerializer(modelName) {
let serializer = this.store.serializerFor(modelName);
return serializer.payloadKeyFromModelName(modelName);
}
emptyResponse(_, options = {}) {
return {
data: options.useValue || null
};
}
/**
* JSONAPISerializer does not use modelName for payload key,
* and just has 'data' as the top level key.
*
* @param modelName
* @param fixture
* @returns {*}
*/
createPayload(modelName, fixture) {
return {
data: fixture
};
}
/**
* Add the included data
*
* @param payload
*/
addIncludedArray(payload) {
if (!isEmpty(this.included)) {
payload.included = this.included;
}
}
/**
In order to conform to the way ember data expects to handle relationships
in a json api payload ( during deserialization ), convert a record ( model instance )
into an object with type and id. Types are pluralized.
@param {Object or DS.Model instance} record
@param {Object} relationship
*/
normalizeAssociation(record) {
if (typeOf(record) === 'object') {
return {
type: this.typeTransformFn(record.type),
id: record.id
};
} else {
return {
type: this.typeTransformFn(record.constructor.modelName),
id: record.id
};
}
}
isEmbeddedRelationship(/*modelName, attr*/
) {
return false;
}
/**
Recursively descend into the fixture json, looking for relationships that are
either record instances or other fixture objects that need to be normalized
and/or included in the 'included' hash
@param modelName
@param fixture
@param included
@returns {{type: *, id: *, attributes}}
*/
convertSingle(modelName, fixture) {
let polymorphicType = fixture.type;
if (polymorphicType && fixture._notPolymorphic) {
polymorphicType = modelName;
delete fixture._notPolymorphic;
}
let data = {
type: this.typeTransformFn(polymorphicType || modelName),
attributes: this.extractAttributes(modelName, fixture)
};
this.addPrimaryKey(modelName, data, fixture);
let relationships = this.extractRelationships(modelName, fixture);
this.verifyLinks(modelName, fixture.links);
this.assignLinks(relationships, fixture.links);
if (Object.getOwnPropertyNames(relationships).length > 0) {
data.relationships = relationships;
}
return data;
}
/*
Add the model to included array unless it's already there.
*/
addToIncluded(data) {
let found = A(this.included).find(model => {
return model.id === data.id && model.type === data.type;
});
if (!found) {
this.included.push(data);
}
}
addToIncludedFromProxy(proxy) {
proxy.includes().forEach(data => {
this.addToIncluded(data);
});
}
assignRelationship(object) {
return {
data: object
};
}
/**
* JSONAPI can have data and links in the same relationship definition
* so do special handling to make it happen
*
* json = build('user', {links: {company: '/user/1/company'}});
*
* {
* data: {
* id: 1,
* type: 'user',
* attributes: {
* name: 'User1',
* style: "normal"
* },
* relationships: {
* company: {
* links: {related: '/user/1/company'}
* }
* }
* }
*
* @param relationshipData
* @param links
*/
assignLinks(relationshipData, links) {
for (let [relationshipKey, link] of entries(links || {})) {
let data = relationshipData[relationshipKey];
data = Object.assign({
links: {
related: link
}
}, data);
relationshipData[relationshipKey] = data;
}
}
}
class FixtureBuilder {
constructor(store, converterClass, payloadClass) {
this.store = store;
this.converterClass = converterClass;
this.payloadClass = payloadClass;
}
getConverter(options) {
return new this.converterClass(this.store, options);
}
wrapPayload(modelName, json, converter = this.getConverter()) {
new this.payloadClass(modelName, json, converter);
}
/**
* Transform an attribute key to what the serializer would expect.
* Key should be attribute but relationships still work.
*
* @param modelName
* @param key
* @returns {*}
*/
transformKey(modelName, key) {
let converter = this.getConverter(),
model = this.store.modelFor(modelName),
relationshipsByName = model.relationshipsByName,
relationship = relationshipsByName.get(key);
if (relationship) {
return converter.transformRelationshipKey(relationship, modelName);
}
let transformKeyFunction = converter.getTransformKeyFunction(modelName, 'Attribute');
return transformKeyFunction(key);
}
/**
Normalizes the serialized model to the expected API format
@param modelName
@param payload
*/
normalize(modelName, payload) {
return payload;
}
/**
Convert fixture for FactoryGuy.build
@param modelName
@param fixture
@param converterOptions
*/
convertForBuild(modelName, fixture, converterOptions) {
let converter = this.getConverter(converterOptions);
if (!fixture) {
return converter.emptyResponse(modelName, converterOptions);
}
let json = converter.convert(modelName, fixture);
this.wrapPayload(modelName, json, converter);
return json;
}
/**
Convert to the ember-data JSONAPI adapter specification, since FactoryGuy#make
pushes jsonapi data into the store. For make builds, don't transform attr keys,
because the json is being pushed into the store directly
( not going through adapter/serializer layers )
@param {String} modelName
@param {String} fixture
@returns {*} new converted fixture
*/
convertForMake(modelName, fixture) {
let converter = new JSONAPIFixtureConverter(this.store, {
transformKeys: false
});
return converter.convert(modelName, fixture);
}
/**
Convert simple ( older ember data format ) error hash:
{errors: {description: ['bad']}}
to:
{errors: [{detail: 'bad', source: { pointer: "data/attributes/description"}, title: 'invalid description'}] }
@param errors simple error hash
@returns {{}} JSONAPI formatted errors
*/
convertResponseErrors(object) {
let jsonAPIErrors = [],
{
errors
} = object;
assert(`[ember-data-factory-guy] Your error response must have an errors key.
The errors hash format is: {errors: {name: ["name too short"]}}`, errors);
for (let key in errors) {
let description = typeOf(errors[key]) === 'array' ? errors[key][0] : errors[key],
source = {
pointer: 'data/attributes/' + key
},
newError = {
detail: description,
title: 'invalid ' + key,
source: source
};
jsonAPIErrors.push(newError);
}
return {
errors: jsonAPIErrors
};
}
}
class BasePayload {
/**
Proxy class for getting access to a json payload.
Allows you to:
- inspect a payload with friendly .get(attr) syntax
- add to json payload with more json built from build and buildList methods.
@param {String} modelName name of model for payload
@param {Object} json json payload being proxied
@param {Boolean} converter the converter that built this json
*/
constructor(modelName, json, converter) {
this.modelName = modelName;
this.json = json;
this.converter = converter;
this.listType = converter.listType || false;
this.proxyMethods = w('getModelPayload isProxy get add unwrap');
this.wrap(this.proxyMethods);
}
/**
Add another json payload or meta data to this payload
Typically you would build a payload and add that to another one
Usage:
```
let batMen = buildList('bat_man', 2);
let user = build('user').add(batMen);
```
but you can also add meta data:
```
let user = buildList('user', 2).add({meta: { next: '/url?page=3', previous: '/url?page=1'}});
```
@param {Object} optional json built from FactoryGuy build or buildList or
meta data to add to payload
@returns {Object} the current json payload
*/
add(more) {
this.converter.included = this.json;
A(Object.getOwnPropertyNames(more)).reject(key => A(this.proxyMethods).includes(key)).forEach(key => {
if (typeOf(more[key]) === 'array') {
more[key].forEach(data => this.converter.addToIncluded(data, key));
} else {
if (key === 'meta') {
this.addMeta(more[key]);
} else {
this.converter.addToIncluded(more[key], key);
}
}
});
return this.json;
}
/**
Add new meta data to the json payload, which will
overwrite any existing meta data with same keys
@param {Object} data meta data to add
*/
addMeta(data) {
this.json.meta = this.json.meta || {};
Object.assign(this.json.meta, data);
}
// marker function for saying "I am a proxy"
isProxy() {}
// get the top level model's payload ( without the includes or meta data )
getModelPayload() {
return this.get();
}
// each subclass has unique proxy methods to add to the basic
addProxyMethods(methods) {
this.proxyMethods = this.proxyMethods.concat(methods);
this.wrap(methods);
}
// add proxy methods to json object
wrap(methods) {
methods.forEach(method => this.json[method] = this[method].bind(this));
}
// remove proxy methods from json object
unwrap() {
this.proxyMethods.forEach(method => delete this.json[method]);
}
/**
Main access point for most users to get data from the
json payload
Could be asking for attribute like 'id' or 'name',
or index into list for list type like 0 or 1
@param key
@returns {*}
*/
get(key) {
if (this.listType) {
return this.getListKeys(key);
}
return this.getObjectKeys(key);
}
}
class JSONAPIPayload extends BasePayload {
constructor(modelName, json, converter) {
super(modelName, json, converter);
this.data = json.data;
this.addProxyMethods(['includes']);
}
getModelPayload() {
return this.data;
}
/**
* Override base add method for special json-api handling to
* add more things to payload like meta or more json to sideload
*
* @param more
*/
add(more) {
if (more.meta) {
this.addMeta(more.meta);
} else {
if (!this.json.included) {
this.json.included = [];
}
this.converter.included = this.json.included;
// add the main moreJson model payload
let data = more.getModelPayload();
if (typeOf(data) === 'array') {
data.forEach(dati => this.converter.addToIncluded(dati));
} else {
this.converter.addToIncluded(data);
}
// add all of the moreJson's includes
this.converter.addToIncludedFromProxy(more);
}
return this.json;
}
createAttrs(data) {
let relationships = {};
Object.keys(data.relationships || []).forEach(key => {
relationships[key] = data.relationships[key].data;
});
let attrs = Object.assign({}, data.attributes, relationships);
attrs.id = data.id;
return attrs;
}
includes() {
return this.json.included || [];
}
getObjectKeys(key) {
let attrs = this.createAttrs(this.data);
if (!key) {
return attrs;
}
if (attrs[key]) {
return attrs[key];
}
}
getListKeys(key) {
let attrs = this.data.map(data => this.createAttrs(data));
if (isEmpty(key)) {
return attrs;
}
if (typeof key === 'number') {
return attrs[key];
}
if (key === 'firstObject') {
return attrs[0];
}
if (key === 'lastObject') {
return attrs[attrs.length - 1];
}
}
}
/**
Fixture Builder for JSONAPISerializer
*/
class JSONAPIFixtureBuilder extends FixtureBuilder {
constructor(store) {
super(store, JSONAPIFixtureConverter, JSONAPIPayload);
this.updateHTTPMethod = 'PATCH';
}
}
/**
Convert base fixture to a JSON format payload.
@param store
@constructor
*/
class JSONFixtureConverter extends FixtureConverter {
constructor(store, options) {
super(store, options);
this.defaultKeyTransformFn = underscore;
this.polymorphicTypeTransformFn = underscore;
}
/**
* Can't add to payload since sideloading not supported
*
* @param moreJson
*/
add(/*moreJson*/) {}
/**
* There is no payload key for JSON Serializer
*
* @param modelName
* @param fixture
* @returns {*}
*/
createPayload(_, fixture) {
return fixture;
}
/**
* There is no sideloading for JSON Serializer
*
* @param payload
*/
addIncludedArray(/*payload*/) {}
/**
Convert single record
@param {String} modelName
@param {Object} fixture
*/
convertSingle(modelName, fixture) {
let data = {},
attributes = this.extractAttributes(modelName, fixture),
relationships = this.extractRelationships(modelName, fixture);
Object.keys(attributes).forEach(key => {
data[key] = attributes[key];
});
Object.keys(relationships).forEach(key => {
data[key] = relationships[key];
});
this.addPrimaryKey(modelName, data, fixture);
this.verifyLinks(modelName, fixture.links);
this.assignLinks(data, fixture.links);
return data;
}
transformRelationshipKey(relationship, parentModelName) {
let transformedKey = super.transformRelationshipKey(relationship, parentModelName);
if (relationship.options.polymorphic) {
transformedKey = transformedKey.replace('_id', '');
}
return transformedKey;
}
/**
@param {Object} record
@param {Object} relationship
*/
normalizeAssociation(record, relationship) {
if (this.serializeMode) {
return record.id;
}
if (typeOf(record) === 'object') {
if (relationship.options.polymorphic) {
return {
type: dasherize(record.type),
id: record.id
};
} else {
return record.id;
}
}
// it's a model instance
if (relationship.options.polymorphic) {
return {
type: dasherize(record.constructor.modelName),
id: record.id
};
}
return record.id;
}
/**
* JSON/REST links can be placed in the data exactly as they appear
* on the fixture definition
*
* json = build('user', {links: {properties: '/user/1/properties'}});
*
* {
* user: {
* id: 1,
* name: 'User1',
* style: "normal",
* links: { properties: '/user/1/properties' }
* }
* }
*
* @param data
* @param links
*/
assignLinks(data, links) {
if (!isEmptyObject(links)) {
data.links = links;
}
}
/**
The JSONSerializer does not support sideloading records
@param {String} modelKey
@param {Object} data
@param {Object} includeObject
*/
addToIncluded(/*data, modelKey*/) {}
/**
The JSONSerializer does not support sideloading records
@param proxy json payload proxy
*/
addToIncludedFromProxy(/*proxy*/) {}
}
/**
Convert base fixture to a REST Serializer formatted payload.
@param store
@constructor
*/
class RESTFixtureConverter extends JSONFixtureConverter {
constructor(store, options) {
super(store, options);
this.included = {};
}
emptyResponse(modelName, options = {}) {
return {
[modelName]: options.useValue || null
};
}
/**
* RESTSerializer has a payload key
*
* @param modelName
* @param fixture
* @returns {*}
*/
createPayload(modelName, fixture) {
return {
[this.getPayloadKey(modelName)]: fixture
};
}
/**
* Get the payload key for this model from the serializer
*
* @param modelName
* @returns {*}
*/
getPayloadKey(modelName) {
let serializer = this.store.serializerFor(modelName),
payloadKey = modelName;
// model fragment serializer does not have payloadKeyFromModelName method
if (serializer.payloadKeyFromModelName) {
payloadKey = serializer.payloadKeyFromModelName(modelName);
}
return this.listType ? pluralize(payloadKey) : payloadKey;
}
/**
* Add the included data to the final payload
*
* @param payload
*/
addIncludedArray(payload) {
Object.keys(this.included).forEach(key => {
if (!payload[key]) {
payload[key] = this.included[key];
} else {
Array.prototype.push.apply(payload[key], this.included[key]);
}
});
}
/**
Add the model to included array unless it's already there.
@param {String} modelKey
@param {Object} data
@param {Object} includeObject
*/
addToIncluded(data, modelKey) {
let relationshipKey = pluralize(dasherize(modelKey));
if (!this.included[relationshipKey]) {
this.included[relationshipKey] = [];
}
let modelRelationships = this.included[relationshipKey],
found = A(modelRelationships).find(existing => existing.id === data.id);
if (!found) {
modelRelationships.push(data);
}
}
/**
Add proxied json to this payload, by taking all included models and
adding them to this payloads includes
@param proxy json payload proxy
*/
addToIncludedFromProxy(proxy) {
proxy.includeKeys().forEach(modelKey => {
let includedModels = proxy.getInclude(modelKey);
includedModels.forEach(data => {
this.addToIncluded(data, modelKey);
});
});
}
}
class RESTPayload extends BasePayload {
constructor(modelName, json, converter) {
super(modelName, json, converter);
this.payloadKey = converter.getPayloadKey(modelName);
this.addProxyMethods(['includeKeys', 'getInclude']);
}
includeKeys() {
let keys = A(Object.keys(this.json)).reject(key => this.payloadKey === key);
return A(keys).reject(key => A(this.proxyMethods).includes(key)) || [];
}
getInclude(modelType) {
return this.json[modelType];
}
getObjectKeys(key) {
let attrs = this.json[this.payloadKey];
if (isEmpty(key)) {
return attrs;
}
return attrs[key];
}
getListKeys(key) {
let attrs = this.json[this.payloadKey];
if (isEmpty(key)) {
return attrs;
}
if (typeof key === 'number') {
return attrs[key];
}
if (key === 'firstObject') {
return attrs[0];
}
if (key === 'lastObject') {
return attrs[attrs.length - 1];
}
}
}
/**
Fixture Builder for REST based Serializer, like ActiveModelSerializer or
RESTSerializer
*/
class RESTFixtureBuilder extends FixtureBuilder {
constructor(store) {
super(store, RESTFixtureConverter, RESTPayload);
}
/**
Map single object to response json.
Allows custom serializing mappings and meta data to be added to requests.
@param {String} modelName model name
@param {Object} json Json object from record.toJSON
@return {Object} responseJson
*/
normalize(modelName, payload) {
return {
[modelName]: payload
};
}
}
class JSONPayload extends BasePayload {
/**
Can't add to included array for JSON payloads since they have
no includes or sideloaded relationships
Meta not working at the moment for this serializer even though
it is being included here in the payload
*/
add(more) {
if (more.meta) {
this.addMeta(more.meta);
}
return this.json;
}
getObjectKeys(key) {
let attrs = this.json;
if (isEmpty(key)) {
return JSON.parse(JSON.stringify(attrs));
}
return attrs[key];
}
getListKeys(key) {
let attrs = this.json;
if (isEmpty(key)) {
return JSON.parse(JSON.stringify(attrs));
}
if (typeof key === 'number') {
return attrs[key];
}
if (key === 'firstObject') {
return attrs[0];
}
if (key === 'lastObject') {
return attrs[attrs.length - 1];
}
}
}
/**
Fixture Builder for JSONSerializer
*/
class JSONFixtureBuilder extends FixtureBuilder {
constructor(store) {
super(store, JSONFixtureConverter, JSONPayload);
}
/**
Map single object to response json.
Allows custom serializing mappings and meta data to be added to requests.
@param {String} modelName model name
@param {Object} json Json object from record.toJSON
@return {Object} responseJson
*/
normalize(_, payload) {
return payload;
}
}
/**
Convert base fixture to the ActiveModel Serializer expected payload.
*/
class AMSFixtureConverter extends RESTFixtureConverter {
/**
* In `serializeMode` use convert a relationship from "company" to "company_id"
* which REST / JSON converters override to strip that _id
*
* @param relationship
* @returns {*}
*/
transformRelationshipKey(relationship, parentModelName) {
if (this.serializeMode) {
let transformFn = this.getTransformKeyFunction(relationship.type, 'Relationship');
return transformFn(relationship.name, relationship.kind);
} else {
return super.transformRelationshipKey(relationship, parentModelName);
}
}
}
/**
Fixture Builder for ActiveModelSerializer
*/
class ActiveModelFixtureBuilder extends FixtureBuilder {
constructor(store) {
super(store, AMSFixtureConverter, RESTPayload);
}
/**
ActiveModelAdapter converts them automatically for status 422
@param errors
@returns {*}
*/
convertResponseErrors(errors, status) {
if (status === 422) {
return errors;
} else {
return super.convertResponseErrors(errors, status);
}
}
/**
Map single object to response json.
Allows custom serializing mappings and meta data to be added to requests.
@param {String} modelName model name
@param {Object} json Json object from record.toJSON
@return {Object} responseJson
*/
normalize(modelName, payload) {
return {
[modelName]: payload
};
}
}
let ActiveModelSerializer;
if (macroCondition(dependencySatisfies('active-model-adapter', '*'))) {
ActiveModelSerializer = importSync('active-model-adapter').ActiveModelSerializer;
}
class FixtureBuilderFactory {
constructor(store) {
this.store = store;
}
/**
Return appropriate FixtureBuilder for the model's serializer type
*/
fixtureBuilder(modelName) {
let serializer = this.store.serializerFor(modelName);
if (!serializer) {
return new JSONAPIFixtureBuilder(this.store);
}
if (this.usingJSONAPISerializer(serializer)) {
return new JSONAPIFixtureBuilder(this.store);
}
if (this.usingActiveModelSerializer(serializer)) {
return new ActiveModelFixtureBuilder(this.store);
}
if (this.usingRESTSerializer(serializer)) {
return new RESTFixtureBuilder(this.store);
}
if (this.usingJSONSerializer(serializer)) {
return new JSONFixtureBuilder(this.store);
}
return new JSONAPIFixtureBuilder(this.store);
}
usingJSONAPISerializer(serializer) {
return serializer instanceof JSONAPISerializer;
}
usingActiveModelSerializer(serializer) {
return ActiveModelSerializer && serializer instanceof ActiveModelSerializer;
}
usingRESTSerializer(serializer) {
return serializer instanceof RESTSerializer;
}
usingJSONSerializer(serializer) {
return serializer instanceof JSONSerializer;
}
}
/**
* This request wrapper controls what will be returned by one url / http verb
* Normally when you set up pretender, you give it one function to handle one url / verb.
*
* So, for example, you would:
*
* ```
* pretender.get('/users', myfunction )
* ```
*
* to mock a [GET /users] call
*
* This wrapper allows that GET /users call to be handled my many functions
* instead of just one, since this request handler hold the ability to take
* a list of hanlders.
*
* That way you can setup a few mocks like
*
* ```
* mockFindAll('user')
* mockQuery('user', {name: 'Dude'})
* ```
*
* and both of these hanlders will reside in the list for the wrapper that
* belongs to [GET /users]
*/
class RequestWrapper {
constructor() {
this.index = 0;
this.handlers = [];
return this.generateRequestHandler();
}
/**
* Generating a function that we can hand off to pretender that
* will handle the request.
*
* Before passing back that function, add some other functions
* to control the handlers array
*
* @returns {function(this:T)}
*/
generateRequestHandler() {
let requestHandler = this.handleRequest.bind(this),
methods = ['getHandlers', 'addHandler', 'removeHandler'];
methods.forEach(method => requestHandler[method] = this[method].bind(this));
return requestHandler;
}
/**
* Sort the handlers by those with query params first
*
*/
getHandlers() {
return this.handlers.sort((a, b) => b.hasQueryParams() - a.hasQueryParams());
}
addHandler(handler) {
this.handlers.push(handler);
return this.index++;
}
removeHandler(handler) {
this.handlers = this.handlers.filter(h => h.mockId !== handler.mockId);
}
/**
* This is the method that pretender will call to handle the request.
*
* Flip though the list of handlers to find one that matches and return
* the response if one is found.
*
* Special handling for mock that use query params because they should take precedance over
* a non query param mock find type since they share the same type / url.
*
* So, let's say you have mocked like this:
* ```
* let mockF = mockFindAll('user', 2);
* let mockQ = mockQuery('user', { name: 'Sleepy' });
* ```
* If your code does a query like this:
*
* ```
* store.query('user', { name: 'Sleepy' });
* ```
*
* Even thought the mockFindAll was declared first, the query handler will be used
*
* @param {FakeRequest} request pretenders object
* @returns {[null,null,null]}
*/
handleRequest(request) {
let handler = this.getHandlers(request).find(handler => handler.matches(request));
if (handler) {
let {
status,
headers,
responseText
} = handler.getResponse();
return [status, headers, responseText];
}
}
}
let wrappers = {},
pretender = null,
delay = 0;
/**
* RequestManager controls setting up pretender to handle the mocks that are
* created.
*
* For each request type / url like [GET /users] or [POST /user/1]
* the request manager will assign a RequestWrapper class to handle it's response.
*
* This class will take the mock handler classes and assign them to a wrapper,
* and also allow you to remove the handler or replace it from it's current
* wrapper to new one.
*/
class RequestManager {
/**
* For now, you can only set the response delay.
*
* @param {Number} responseTime
* @returns {{responseTime: number}} the current settings
*/
static settings({
responseTime
} = {}) {
if (isPresent(responseTime)) {
delay = responseTime;
}
// return current settings
return {
responseTime: delay
};
}
static getKey(type, url) {
return [type, url].join(' ');
}
/**
* Give each handler a mockId that is an object that holds information
* about what it is mocking { type, url, num }
*
* @param {String} type like GET or POST
* @param {String} url like '/users'
* @param {Number} num a sequential number for each handler
* @param handler
*/
static assignMockId(type, url, num, handler) {
handler.mockId = {
type,
url,
num
};
}
/**
* Add a handler to the correct wrapper and assign it a mockId
*
* @param handler
*/
static addHandler(handler) {
let {
type,
url
} = this.getTypeUrl(handler),
key = this.getKey(type, url),
wrapper = wrappers[key];
if (!wrapper) {
wrapper = new RequestWrapper();
this.getPretender()[type.toLowerCase()].call(pretender, url, wrapper, delay);
wrappers[key] = wrapper;
}
let index = wrapper.addHandler(handler);
this.assignMockId(type, url, index, handler);
}
/**
* Remove a handler from the wrapper it was in
*
* @param handler
*/
static removeHandler(handler) {
// get the old type, url info from last mockId
// in order to find the wrapper it was in
let {
type,
url
} = handler.mockId,
key = this.getKey(type, url),
wrapper = wrappers[key];
if (wrapper) {
wrapper.removeHandler(handler);
}
}
/**
* Replace a handler from old wrapper to new one
*
* @param handler
*/
static replaceHandler(handler) {
this.removeHandler(handler);
this.addHandler(handler);
}
// used for testing
static findWrapper({
handler,
type,
url
}) {
if (handler) {
type = handler.getType();
url = handler.getUrl();
}
let key = this.getKey(type, url);
return wrappers[key];
}
static getTypeUrl(handler) {
return {
type: handler.getType(),
url: handler.getUrl()
};
}
static reset() {
wrappers = {};
pretender && pretender.shutdown();
pretender = null;
delay = 0;
}
static getPretender() {
if (!pretender) {
pretender = new Pretender();
}
return pretender;
}
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function Sequence (fn) {
let index = 1;
this.next = function () {
return fn.call(this, index++);
};
this.reset = function () {
index = 1;
};
}
function MissingSequenceError (message) {
this.toString = function () {
return message;
};
}
let stringsOnly = false;
if (macroCondition(getOwnConfig()?.useStringIdsOnly)) {
stringsOnly = true;
} else {
stringsOnly = false;
}
/**
* A function to help check if an id conforms to the configuration of useStringIdsOnly
*
* Ensures either the id is string, or they've disabled useStringIdsOnly
*/
function verifyId(id) {
assert(`[ember-data-factory-guy]: non-string id given, but useStringIdsOnly config is set`, typeof id === 'string' || !useStringIdsOnly);
}
const useStringIdsOnly = stringsOnly;
/**
* A wrapper around generated ids to help with handling them, regardless of string vs int id type.
* Keeps track of next id to use for a model definition when creating a new record.
*/
class IdGenerator {
constructor() {
_defineProperty(this, "newId", useStringIdsOnly ? '1' : 1);
}
// should always be an unused id ready for use
/**
* An unused id is requested. Return the unused id, and cycle it to make a new one.
*/
nextId() {
const {
newId
} = this;
this.newId = typeof newId === 'string' ? (parseInt(newId, 10) + 1).toString() : newId + 1;
return newId;
}
}
/**
A ModelDefinition encapsulates a model's definition
@param model
@param config
@constructor
*/
class ModelDefinition {
constructor(model, config) {
this.modelName = model;
this.idGenerator = new IdGenerator();
this.originalConfig = mergeDeep({}, config);
this.parseConfig(Object.assign({}, config));
}
/**
Returns a model's full relationship if the field is a relationship.
@param {String} field field you want to relationship info for
@returns {DS.Relationship} relationship object if the field is a relationship, null if not
*/
getRelationship(field) {
let modelClass = factoryGuy.store.modelFor(this.modelName);
let relationship = modelClass.relationshipsByName.get(field);
return relationship || null;
}
/**
@param {String} name model name like 'user' or named type like 'admin'
@returns {Boolean} true if name is this definitions model or this definition
contains a named model with that name
*/
matchesName(name) {
return this.modelName === name || this.namedModels[name];
}
/**
Call the next method on the named sequence function. If the name
is a function, create the sequence with that function
@param {String} name previously declared sequence name or
an the random name generate for inline functions
@param {Function} sequenceFn optional function to u