"use strict";
import inflect from "inflect";
import JSONSerializer from "@parch-js/json-serializer";
/**
* @class RestSerializer
* @extends <a href="https://github.com/parch-js/json-serializer" target="_blank">JSONSerializer</a>
* @constructor
*/
export default class RestSerializer extends JSONSerializer {
/**
* Returns an array of ids for a give hasMany/belongsToMany relatioship
*
* @method getRelationships
* @param {Object} instance Sequelize model instance
* @param {Object} association Sequelize model instance
* @return {Array}
*
* @example
* ```javascript
* return orm.findOne("user", 1).then(user => {
* return serializer.getRelationships(user, user.Model.associations.Project);
* }).then(relationships => {
* /**
* * [1, 2, 3]
* *
* });
* ```
*/
getRelationships(instance, association) {
const accessors = association.accessors;
const isManyRelationship = Object.keys(accessors).some(accessor => {
const hasManyRelationshipAccessor = [
"add",
"addMultiple",
"count",
"hasSingle",
"hasAll",
"removeMultiple"
].some(valid => valid === accessor);
return hasManyRelationshipAccessor;
});
if (isManyRelationship) {
return instance[accessors.get]().then(relationships =>
relationships.map(relationship => relationship.id)
);
} else {
return Promise.resolve();
}
}
/**
* Returns the name string for the record
*
* @method keyForRecord
* @param {Object} instance sequelize model instance
* @param {Boolean} singular singular or plural name
* @return {String} name string for record root
*
* @example
* ```javascript
* return orm.findOne("user", 1).then(user => {
* const res = {};
* const resKey = serializer.keyForRecord(user, true);
*
* res[resKey] = user.toJSON();
*
* return res;
* });
* ```
*/
keyForRecord(instance, singular) {
const tableName = instance.Model.getTableName();
const recordKey = tableName.toLowerCase();
if (singular) { return inflect.singularize(recordKey); }
return inflect.pluralize(recordKey);
}
/**
* Return the object key for a relationship
*
* @method keyForRelationship
* @param {String} relationship the relationship name (e.g. `Projects`)
* @return {String} name string for the relationship
*
* @example
* ```javascript
* return serializer.keyForRelationship("Projects").then(key => {
* // "projects"
* });
* ```
*/
keyForRelationship(relationship) {
return inflect.pluralize(relationship.toLowerCase());
}
/**
* Takes an array of Sequelize instances and returns an object with a root key
* based on the model name and an array of pojo records
*
* @method normalizeArrayResponse
* @param {Array} instances Sequelize instances
* @return {Promise}<Object, Error>
*
* @example
* ```javascript
* return orm.findAll("user").then(users => {
* return serializer.normalizeArrayResponse(instances);
* }).then(response => {
* /**
* * {
* * users: [{
* * }]
* * }
* });
* ```
*/
normalizeArrayResponse(instances, fallbackName) {
let key;
if (!instances || !instances.length) {
key = inflect.camelize(inflect.pluralize(fallbackName), false);
return Promise.resolve({
[key]: []
});
}
return Promise.all(instances.map(instance => {
key = key || this.keyForRecord(instance, false);
return this.normalizeRelationships(instance, instance);
})).then(records => this._defineArrayResponse(key, records));
}
/**
* Takes a single Sequelize instance and returns an object with a root key based
* on the model name and a pojo record
*
* @method normalizeSingularResponse
* @param {Object} instance Sequelize model instance
* @return {Promise}<Object, Error>
*
* @example
* ```javascript
* return orm.findOne("user", 1).then(user => {
* return serializer.normalizeSingularResponse(instance, "findOne");
* }).then(response => {
* /**
* * {
* * user: {
* * }
* * }
* });
* ```
*/
normalizeSingularResponse(instance) {
const key = this.keyForRecord(instance, true);
return this.normalizeRelationships(instance, instance).then(newRecord =>
this._defineSingularResponse(key, newRecord)
);
}
/**
* @method normalizeRelationships
* @param {Object} instance Sequelize model instance
* @param {Object} payload Pojo representation of Sequelize model instance
* @return {Promis}<Object, Error>
*
* @example
* ```javascript
* return store.findOne("user", 1).then(user => {
* return serializer.normalizeRelationships(user, user.toJSON());
* }).then(response => {
* /**
* * {
* * user: {
* * projects: [1, 2, 3]
* * }
* * }
* });
* ```
*/
normalizeRelationships(instance, payload) {
const associations = instance.Model.associations;
return Promise.all(Object.keys(associations).map(association =>
this.getRelationships(instance, associations[association]).then(relationships => {
return {
key: this.keyForRelationship(association),
records: relationships
};
})
)).then(relationships => {
relationships.forEach(relationship => {
if (relationship.records) {
payload[relationship.key] = relationship.records;
}
});
return payload;
});
}
/**
* Reformats the record into a RESTful object with the record name as the key.
* In addition, this will add a custom toJSON method on the response object
* that will serialize the response when sent through something like
* express#res.send, retaining the relationships on the instance, but removing
* all other extraneous data (see <a href="https://github.com/sequelize/sequelize/blob/16864699e0cc4b5fbc5bbf742b7a15eea9948e77/lib/model.js#L4005" target="_bank">Sequelize instance#toJSON</a>)
*
* @method _defineArrayResponse
* @private
* @param {String} key the name of the record (e.g. users)
* @param {Array<Object>} records Array of sequelize instances
* @returns {Object}
*
* @example
* ```javascript
* serializer._defineArrayResponse("users", [{
* dataValues: {
* firstName: "Hank",
* lastName: "Hill",
* projects: [1, 2]
* },
* someExtraneousProp: "foo"
* }]);
*
* /**
* * {
* * users: [{
* * dataValues: {
* * firstName: "Hank",
* * lastName: "Hill",
* * projects: [1, 2],
* * },
* * someExtraneousProp: "foo",
* * toJSON() {
* * }
* * }]
* * }
* *
* * response.toJSON()
* *
* * {
* * "users": [{
* * firstName: "Hank",
* * lastName: "Hill",
* * projects: [1, 2],
* * }]
* * }
* ```
*/
_defineArrayResponse(key, records) {
const response = {};
Object.defineProperty(response, key, {
configurable: false,
enumerable: true,
value: records
});
Object.defineProperty(response, "toJSON", {
configurable: false,
enumerable: false,
value() {
const recordArray = this[key];
const newRecords = recordArray.map(record => {
const associations = record.Model.associations;
const newRecord = {};
Object.keys(associations).forEach(association => {
if (record[association]) {
newRecord[association] = record[association];
}
});
const plainInstance = record.toJSON();
Object.keys(plainInstance).forEach(property => {
newRecord[property] = plainInstance[property];
});
return newRecord;
});
return {
[key]: newRecords
};
}
});
return response;
}
/**
* Similar to {{#crossLink "RestSerializer/_defineArrayResponse:method"}}_defineArrayResponse{{/crossLink}},
* the difference being that this takes a single record and returns a singular response
*
* @method _defineSingularResponse
* @private
* @param {String} key the name of the record (e.g. users)
* @param {Object} record Sequelize instance
* @returns {Object}
*
* @example
* ```javascript
* serializer._defineSingularResponse("user", {
* dataValues: {
* firstName: "Hank",
* lastName: "Hill",
* projects: [1, 2]
* },
* someExtraneousProp: "foo",
* });
*
* /**
* * {
* * user: {
* * dataValues: {
* * firstName: "Hank",
* * lastName: "Hill",
* * projects: [1, 2],
* * someExtraneousProp: "foo",
* * toJSON() {
* * }
* * }
* * }
* *
* * response.toJSON()
* *
* * {
* * "user": [{
* * "firstName": "Hank",
* * "lastName": "Hill",
* * "projects": [1, 2],
* * }]
* * }
* ```
*/
_defineSingularResponse(key, record) {
const response = {};
Object.defineProperty(response, key, {
configurable: false,
enumerable: true,
value: record
});
Object.defineProperty(response, "toJSON", {
configurable: false,
enumerable: false,
value() {
const instance = this[key];
const associations = instance.Model.associations;
const newRecord = {};
Object.keys(associations).forEach(association => {
if (instance[association]) {
newRecord[association] = instance[association];
}
});
const plainInstance = instance.toJSON();
Object.keys(plainInstance).forEach(property => {
newRecord[property] = plainInstance[property];
});
return {
[key]: newRecord
};
}
});
return response;
}
}