UNPKG

cherrypie.js

Version:

Populate/desolate (alias convert) a Rich Model Object from/to JSON

290 lines (251 loc) 10.4 kB
var _ = require('lodash'); var oKeys = _.keys; var get = _.get; var set = _.set; var diff = _.difference; var intersect = _.intersection; var union = _.union; var compact = _.compact; var isFunction = _.isFunction; var isArray = _.isArray; var logPrefix = '<cherrypie>'; function filterMetaKeyNames (name) { return !/^__/.test(name); } function checkForInjections (modelDescription) { var injections = modelDescription.__inject; var hasInjections = false; if (injections) { var injectionDesc = injections.toString(); hasInjections = typeof injections === 'object' && injectionDesc === '[object Object]'; // only accepting POJOs if (!hasInjections) { throw new Error(logPrefix + ' The specified `__inject`-property has to be a plain JS Object - you passed "' + injectionDesc + '" instead!'); } } return hasInjections; } /** * Use cherrypie.js to populate models from an (JSON) origin and desolate rich models in an intuitive way. * * @class Cherrypie * @module cherrypie.js * @static */ module.exports = { /** * Takes the given model description along with the origin (JSON) object * and returns the populated model. * * @method populate * @param {ModelDescription|Object} modelDescription The particular model description object. * @param {JSON|Object} origin The particular JSON origin. * @returns {Object} */ populate: function (modelDescription, origin) { var context = this; var preparedOrigin = modelDescription.__namespace ? get(origin, modelDescription.__namespace) : origin; var childDescriptions = modelDescription.__children || {}; var shouldTransferKeys = !!modelDescription.__transferKeys; var shouldIgnoreKeys = shouldTransferKeys && !!modelDescription.__ignoredKeys; var hasInjections = checkForInjections(modelDescription); var transferredKeys = []; var keys = oKeys(modelDescription).filter(filterMetaKeyNames); var computedProperties = keys.filter(function (key) { return isFunction(modelDescription[key]); }); var model = {}; keys = diff(keys, computedProperties); // this._checkForChildren(keys, oKeys(childDescriptions)); if (shouldTransferKeys) { transferredKeys = this._getTransferredKeys(keys, preparedOrigin, modelDescription, childDescriptions); } else { this._checkForChildren(keys, oKeys(childDescriptions)); } if (shouldIgnoreKeys) { transferredKeys = diff(transferredKeys, modelDescription.__ignoredKeys); } if (preparedOrigin) { keys.forEach(function (key) { var value = modelDescription[key]; var childDescription = childDescriptions[key]; var parsedValue = preparedOrigin.hasOwnProperty(value) ? preparedOrigin[value] : get(preparedOrigin, value); if (childDescription) { if (isArray(parsedValue)) { parsedValue = parsedValue.map(function (child) { return context.populate(childDescription, child); }).filter(function (populatedChild) { return !!populatedChild; }); } else if (parsedValue && typeof parsedValue === 'object') { parsedValue = context.populate(childDescription, parsedValue); } } model[key] = parsedValue; }); if (shouldTransferKeys) { diff(transferredKeys, keys).forEach(function (transferredKey) { model[transferredKey] = preparedOrigin[transferredKey]; }); } } if (preparedOrigin) { computedProperties.forEach(function (key) { model[key] = modelDescription[key].call(model, preparedOrigin, context); }); } else { computedProperties.forEach(function (key) { model[key] = null; }); } if (hasInjections) { var injectObj = modelDescription.__inject; for (var injectionKey in injectObj) { if (injectObj.hasOwnProperty(injectionKey)) { model[injectionKey] = injectObj[injectionKey]; } } } if (!oKeys(model).length) { model = null; } return model; }, /** * Checks if all `childKeys` are present in the parent model-description keys * and throws an `Error` if not. * * @method _checkForChildren * @param modelDescriptionKeys * @param childKeys * @returns {Boolean} `true` if all child-keys are also present in the parent model-description. * @throws {Error} if not all child-keys are defined in the parent model-description. * @private */ _checkForChildren: function (modelDescriptionKeys, childKeys) { return childKeys.every(function (key) { if (modelDescriptionKeys.indexOf(key) !== -1) { return true; } else { throw new Error(logPrefix + ' Child "' + key + '" declared in __children but not in parent model description!'); } }); }, /** * Reduces the given model object to contain only the properties declared * in the `modelDescription.serializable` Array so that it could be sent * to the backend without all the "junk". * <p> * If the model-description lacks of the `serializable` property, all * keys of the model-description will be serialized. * * @param {ModelDescription|Object} modelDescription The particular model description. * @param {Object} model The model to be desolated/reduced. * @returns {Object} The plain desolated/reduced model. */ desolate: function (modelDescription, model) { var context = this; var namespace = modelDescription.__namespace; var serializablePropertyNames = modelDescription.__serializable; var serializedModel = null; var childModels = null; if (!isArray(serializablePropertyNames)) { serializablePropertyNames = oKeys(modelDescription).filter(filterMetaKeyNames); } // check if any computed properties are defined to be serialized serializablePropertyNames.forEach(function checkForComputedProperties (name) { if (typeof modelDescription[name] === 'function') { throw new Error( logPrefix + ' Unable to desolate a computed property ("' + name + '") found in the Array ' + 'of serializable properties @ namespace "' + namespace + '"!' ); } }); if (modelDescription.__children) { var childKeys = oKeys(modelDescription.__children); childModels = {}; childKeys.forEach(function (childKey) { var keyIndex = serializablePropertyNames.indexOf(childKey); if (keyIndex !== -1) { serializablePropertyNames.splice(keyIndex, 1); childModels[childKey] = context.desolate(modelDescription.__children[childKey], model[childKey]); } }); } serializedModel = this._serialize(modelDescription, model, serializablePropertyNames); if (serializedModel && childModels) { for (var childModel in childModels) { if (childModels.hasOwnProperty(childModel)) { serializedModel[modelDescription[childModel]] = childModels[childModel]; } } } if (namespace) { serializedModel = set({}, namespace, serializedModel); } return serializedModel; }, /** * Serializes the model according to the modelDescription and the serializable properties given. * * @method _serialize * @param {Object} modelDescription The description for this model. * @param {Object|Array} model The model. * @param {Array} serializableProperties The array of serializable model properties. * @returns {Object|Array} * @private */ _serialize: function (modelDescription, model, serializableProperties) { var serialized; if (isArray(model) && model.length) { serialized = model.map(function (obj) { var serializedObj = {}; var objKeys = intersect(oKeys(obj), serializableProperties); objKeys.forEach(function (key) { serializedObj[modelDescription[key]] = obj[key]; }); return serializedObj; }); } else { serialized = {}; intersect(oKeys(model), serializableProperties).forEach(function (validPropKey) { serialized[modelDescription[validPropKey]] = model[validPropKey]; }); } return serialized; }, /** * Extracts the "transferred" keys (the keys which are not defined within the model description and should * therefor be copied unprocessed into the resulting model). * * @method _getTransferredKeys * @param [String] processedKeys The list of processed keys which are described within the * model description. * @param {Object} preparedOrigin The prepared JSON origin object. * @param {Object} modelDescription The current model description object. * @param {Object} childDescriptions The `__children` property of the current model description. * @returns {[String]} * @private */ _getTransferredKeys: function getTransferredKeys (processedKeys, preparedOrigin, modelDescription, childDescriptions) { var descriptionValues = compact(oKeys(modelDescription).filter(filterMetaKeyNames).map(function (modelKey) { var value = modelDescription[modelKey]; if (typeof value === 'string') { return value; } })); oKeys(childDescriptions).forEach(function (childPropertyKey) { var sanitizedChildPropertyKey = modelDescription[childPropertyKey] && modelDescription[childPropertyKey].split( '.')[0]; if (sanitizedChildPropertyKey) { descriptionValues.push(sanitizedChildPropertyKey); } }); return union(oKeys(preparedOrigin).filter(filterMetaKeyNames), processedKeys).filter(function (unionKey) { return descriptionValues.indexOf(unionKey) === -1; }); } };