Files
threshold-ui/addons/api/addon/serializers/application.js
T

241 lines
7.8 KiB
JavaScript
Raw Normal View History

2020-05-19 16:33:31 -04:00
import RESTSerializer from '@ember-data/serializer/rest';
import { underscore } from '@ember/string';
import { get } from '@ember/object';
import { copy } from 'ember-copy';
2020-05-19 16:33:31 -04:00
/**
* Manages serialization/normalization of data to/from the API.
*/
export default class ApplicationSerializer extends RESTSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = true;
2020-05-19 16:33:31 -04:00
// =methods
/**
* Generates an underscored key for the attribute.
* @param {string} attr
* @return {string}
*/
keyForAttribute(attr) {
return underscore(attr);
}
/**
* If an attribute is annotated as readOnly in the model, don't serialize it.
* Otherwise delegate to default attribute serializer.
*
* @override
2020-05-19 16:33:31 -04:00
* @method serializeAttribute
* @param {Snapshot} snapshot
* @param {Object} json
* @param {String} key
* @param {Object} attribute
*/
serializeAttribute(snapshot, json, key, attribute) {
const { type, options } = attribute;
let value = super.serializeAttribute(...arguments);
// Convert empty strings to null.
if (type === 'string' && json[key] === '') json[key] = null;
// Do not serialize read-only attributes.
if (options.readOnly) delete json[key];
2020-08-21 08:55:07 -04:00
// Version is sent only if it has a non-nullish value
if (key === 'version') {
if (json[key] === null || json[key] === undefined) delete json[key];
}
// Do not serialize `disabled` fields.
// TODO: disabled is temporarily disabled
if (key === 'disabled') delete json[key];
2022-01-18 13:44:27 -05:00
// Push nested attributes down into the attributes key
2022-01-19 12:16:57 -05:00
if (options.isNestedAttribute && json[key] !== undefined) {
2022-01-18 13:44:27 -05:00
if (!json.attributes) json.attributes = {};
json.attributes[key] = json[key];
delete json[key];
}
return value;
2020-05-19 16:33:31 -04:00
}
/**
* In our API, request payloads are unrooted. But Ember Data roots request
* payloads for this adapter by default. Instead of assigned a root key
* on the outgoing hash, we copy the serialized attributes into it.
*
* @override
* @method serializeIntoHash
* @param {Object} hash
* @param {Model} typeClass
* @param {Snapshot} snapshot
* @param {Object} options
*/
serializeIntoHash(hash, typeClass, snapshot, options) {
const serialized = this.serialize(snapshot, options);
Object.assign(hash, serialized);
}
2020-09-03 14:35:05 -04:00
/**
* Optionally serializes the scope ID into `scope_id`.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
const serialized = super.serialize(...arguments);
2020-09-10 18:32:17 -04:00
// Delete `scope` field
delete serialized.scope;
// And serialize `scope_id`
2021-03-04 15:47:38 -05:00
if (this.serializeScopeID) {
const scope_id = snapshot?.attr('scope')?.attr('scope_id');
if (scope_id) serialized.scope_id = scope_id;
}
2020-09-03 14:35:05 -04:00
return serialized;
}
2020-05-19 16:33:31 -04:00
/**
* In our API, array payloads are always rooted under the same key `items`.
* Ember Data normally expects them to be rooted under their model name,
* e.g. `{hostCatalogs: […]}`. This method makes this transformation to
2020-05-19 16:33:31 -04:00
* accommodate Ember Data.
*
* @override
* @param {object} store
* @param {object} primaryModelClass
* @param {object} payload
* @param {?string} id
* @param {string} requestType
* @return {object}
*/
normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) {
// Setup a new payload data structure.
const transformedPayload = {};
// Find the Ember-data-expected root key name.
const payloadKey = this.payloadKeyFromModelName(
primaryModelClass.modelName
);
// Copy the data rooted under `items` in the existing payload into the new
// payload under the expected root key name.
transformedPayload[payloadKey] = copy(payload.items, true);
// Return the result of normalizing the transformed payload.
return super.normalizeArrayResponse(
store,
primaryModelClass,
transformedPayload,
id,
requestType
);
}
/**
* In our API, singluar resources are _unrooted_, whereas Ember Data expects
2020-07-15 17:17:11 -04:00
* them to be rooted under their model name, e.g. `{modelName: {…}}`.
* This method makes this transformation to accommodate Ember Data.
*
* @method normalizeSingleResponse
* @override
* @param {Store} store
* @param {Model} primaryModelClass
* @param {Object} payload
* @param {String|Number} id
* @param {String} requestType
* @return {Object} JSON-API Document
*/
normalizeSingleResponse(store, primaryModelClass, payload, id, requestType) {
// Copy payload (mostly to prevent mocking issues)
payload = copy(payload, true);
// Check for and normalize missing arrays
payload = this.normalizeMissingArrays(store, primaryModelClass, payload);
// Setup a new payload data structure.
const transformedPayload = {};
// Find the Ember-data-expected root key name.
const payloadKey = this.payloadKeyFromModelName(
primaryModelClass.modelName
);
// Copy the unrooted payload under the expected root key name.
transformedPayload[payloadKey] = payload;
// Return the result of normalizing the transformed payload.
return super.normalizeSingleResponse(
store,
primaryModelClass,
transformedPayload,
id,
requestType
);
}
/**
* Resets missing array fields to an empty array if they are annotated by
* `normalizeToEmptyArray: true` in the associated model attribute
* declaration. Our API excludes arrays when they are empty from
* singular responses.
*
* @param {Store} store
* @param {Model} primaryModelClass
* @param {object} payload
* @return {object}
*/
normalizeMissingArrays(store, primaryModelClass, payload) {
2020-09-16 14:29:02 -04:00
const attrDefs = store._attributesDefinitionFor(
primaryModelClass.modelName
);
if (attrDefs) {
2020-09-16 14:29:02 -04:00
Object.keys(attrDefs).forEach((key) => {
if (!payload[key] && attrDefs[key]?.options?.emptyArrayIfMissing) {
payload[key] = [];
}
});
}
return payload;
}
/**
* One consequence of using model fragments to represent the embedded scope
* is that they cannot have `id` fields. We still need to know scope IDs,
* so we copy the `scope.id` value into the `scope.scope_id` field.
* @override
* @see FragmentScope
* @param {Model} typeClass
* @param {Object} hash
* @return {Object}
*/
normalize(typeClass, hash) {
2022-01-18 13:44:27 -05:00
let normalizedHash = copy(hash, true);
const scopeID = get(normalizedHash, 'scope.id');
if (scopeID) normalizedHash.scope.scope_id = scopeID;
2022-01-18 13:44:27 -05:00
normalizedHash = this.normalizeNestedAttributes(typeClass, normalizedHash);
2022-01-21 12:43:45 -05:00
// Delete any lingering values on write-only fields
typeClass.attributes.forEach((attribute) => {
if (attribute.options.writeOnly) hash[attribute.name] = null;
});
return super.normalize(typeClass, normalizedHash);
}
2022-01-18 13:44:27 -05:00
/**
* Certain typed resources in the API have type-specific fields nested under
* an `attributes` key in the payload
* (e.g. `attributes: { fieldName: '123' }`). Since Ember Data lacks
* first-class support for nested attributes, these fields must be hoisted up
* into the main body of the payload.
*
* In order to annotate a field as being a nested attribute, add the key name
* to the `nestedAttributes` field on the serializer:
* `@attr('string', { isNestedAttribute: true }) fieldName;`
*
* @param {Model} typeClass
* @param {Object} hash
* @return {Object}
*/
normalizeNestedAttributes(typeClass, hash) {
const normalizedHash = copy(hash, true);
typeClass.attributes.forEach((attribute) => {
const { isNestedAttribute } = attribute.options;
const attributeValue = normalizedHash.attributes?.[attribute.name];
if (isNestedAttribute && attributeValue) {
normalizedHash[attribute.name] = attributeValue;
}
});
return normalizedHash;
}
2020-05-19 16:33:31 -04:00
}