UNPKG

bookshelf-jsonapi-params

Version:

Automatically applies relations, filters, and more from the JSON API spec to your Bookshelf.js results

776 lines (637 loc) 166 kB
'use strict';Object.defineProperty(exports, "__esModule", { value: true });var _slicedToArray = function () {function sliceIterator(arr, i) {var _arr = [];var _n = true;var _d = false;var _e = undefined;try {for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {_arr.push(_s.value);if (i && _arr.length === i) break;}} catch (err) {_d = true;_e = err;} finally {try {if (!_n && _i["return"]) _i["return"]();} finally {if (_d) throw _e;}}return _arr;}return function (arr, i) {if (Array.isArray(arr)) {return arr;} else if (Symbol.iterator in Object(arr)) {return sliceIterator(arr, i);} else {throw new TypeError("Invalid attempt to destructure non-iterable instance");}};}(); // Load modules /* eslint-disable prefer-const */ var _lodash = require('lodash'); var _splitString = require('split-string');var _splitString2 = _interopRequireDefault(_splitString); var _inflection = require('inflection');var _inflection2 = _interopRequireDefault(_inflection); var _bookshelfPage = require('bookshelf-page');var _bookshelfPage2 = _interopRequireDefault(_bookshelfPage); var _jsonFields = require('./json-fields');var _jsonFields2 = _interopRequireDefault(_jsonFields);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}function _defineProperty(obj, key, value) {if (key in obj) {Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });} else {obj[key] = value;}return obj;}function _toConsumableArray(arr) {if (Array.isArray(arr)) {for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {arr2[i] = arr[i];}return arr2;} else {return Array.from(arr);}} /** * Exports a plugin to pass into the bookshelf instance, i.e.: * * import config from './knexfile'; * import knex from 'knex'; * import bookshelf from 'bookshelf'; * * const Bookshelf = bookshelf(knex(config)); * * Bookshelf.plugin('bookshelf-jsonapi-params'); * * export default Bookshelf; * * The plugin attaches the `fetchJsonApi` instance method to * the Bookshelf Model object. * * See methods below for details. */exports.default = function (Bookshelf) {var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; (0, _lodash.defaults)(options, { nullString: 'null' }); // Load the pagination plugin if its not already there if (!(0, _lodash.get)(Bookshelf, 'Model.fetchPage')) { Bookshelf.plugin(_bookshelfPage2.default); } /** * Similar to {@link Model#fetch} and {@link Model#fetchAll}, but specifically * uses parameters defined by the {@link https://jsonapi.org|JSON API spec} to * build a query to further refine a result set. * * @param opts {object} * Currently supports the `include`, `fields`, `sort`, `page` and `filter` * parameters from the {@link https://jsonapi.org|JSON API spec}. * @param type {string} * An optional string that specifies the type of resource being retrieved. * If not specified, type will default to the name of the table associated * with the model. * @return {Promise<Model|Collection|null>} */ var fetchJsonApi = function fetchJsonApi(opts) {var isCollection = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;var _this = this;var type = arguments[2];var additionalQuery = arguments[3]; opts = (0, _lodash.cloneDeep)(opts) || {}; var internals = {};var _opts = opts,include = _opts.include,fields = _opts.fields,sort = _opts.sort,_opts$page = _opts.page,page = _opts$page === undefined ? {} : _opts$page,filter = _opts.filter,group = _opts.group; var filterTypes = ['like', 'not', 'lt', 'gt', 'lte', 'gte', 'or', 'and']; // Do not add the global flag. The global flag will influence String.prototype.match and will // return a list of matches instead of matching groups. Changing this will break existing code. var aggregateFunctionRegex = /(count|sum|avg|max|min)\((.+)\)/; // Get a reference to the field being used as the id internals.idAttribute = this.constructor.prototype.idAttribute ? this.constructor.prototype.idAttribute : 'id'; // Get a reference to the current model name. Note that if no type is // explicitly passed, the tableName will be used internals.modelName = this.constructor.prototype.tableName; internals.modelType = type; // Used to determine which casting syntax is valid internals.client = Bookshelf.knex.client.config.client; // Initialize an instance of the current model and clone the initial query internals.model = this.constructor.forge().query(function (qb) {return (0, _lodash.assign)(qb, _this.query().clone());}); /** * Build a query for single filtering dependency object * @param value {object} * @param relationHash {object} */ internals.buildObjectLikeFilterDependencies = function (value, relationHash) { if (!(0, _lodash.isEmpty)(value)) { (0, _lodash.forEach)(value, function (_, typeKey) { // Add relations to the relationHash internals.buildDependenciesHelper(typeKey, relationHash); }); } }; /** * Build a query for array of relational dependencies of filtering * @param filterList {array} * @param relationHash {object} */ internals.buildOrFilterDependencies = function (filterList, relationHash) { if (!(0, _lodash.isEmpty)(filterList)) { (0, _lodash.forEach)(filterList, function (value) { if ((0, _lodash.isPlainObject)(value)) { internals.buildDependenciesLoopHelper(value, relationHash); } }); } }; internals.buildDependenciesLoopHelper = function (filterValues, relationHash) { // Loop through each filter value (0, _lodash.forEach)(filterValues, function (value, key) { // If the filter is "OR" or "AND" filter fragments array if ((0, _lodash.isArray)(value) && (key === 'or' || key === 'and')) { internals.buildOrFilterDependencies(value, relationHash); } // If the filter is not an equality filter if ((0, _lodash.isObjectLike)(value) && !(0, _lodash.isArray)(value)) { internals.buildObjectLikeFilterDependencies(value, relationHash); } // If the filter is an equality filter else { internals.buildDependenciesHelper(key, relationHash); } }); }; /** * Build a query for relational dependencies of filtering, grouping and sorting * @param filterValues {object} * @param groupValues {object} * @param sortValues {object} */ internals.buildDependencies = function (filterValues, groupValues, sortValues) { var relationHash = {}; // Find relations in filterValues if ((0, _lodash.isObjectLike)(filterValues) && !(0, _lodash.isEmpty)(filterValues)) { internals.buildDependenciesLoopHelper(filterValues, relationHash); } // Find relations in sortValues if ((0, _lodash.isObjectLike)(sortValues) && !(0, _lodash.isEmpty)(sortValues)) { // Loop through each sort value (0, _lodash.forEach)(sortValues, function (value) { // If the sort value is descending, remove the dash if (value.indexOf('-') === 0) { value = value.substr(1); } // Add relations to the relationHash internals.buildDependenciesHelper(value, relationHash); }); } // Find relations in groupValues if ((0, _lodash.isObjectLike)(groupValues) && !(0, _lodash.isEmpty)(groupValues)) { // Loop through each group value (0, _lodash.forEach)(groupValues, function (value) { // Add relations to the relationHash internals.buildDependenciesHelper(value, relationHash); }); } // Need to select model.* so all of the relations are not returned, also check if there is anything in fields object if ((0, _lodash.keys)(relationHash).length && !(0, _lodash.keys)(fields).length) { internals.model.query(function (qb) { qb.select(internals.modelName + '.*'); }); } // Recurse on each of the relations in relationHash (0, _lodash.forIn)(relationHash, function (value, key) { return internals.queryRelations(value, key, _this, internals.modelName); }); }; /** * Recursive funtion to add relationships to main query to allow filtering and sorting * on relationships by using left outer joins * @param relation {object} * @param relationKey {string} * @param parent {object} * @param parentKey {string} */ internals.queryRelations = function (relation, relationKey, parentModel, parentKey) { // Add left outer joins for the relation var relatedData = parentModel[relationKey]().relatedData; internals.model.query(function (qb) { var foreignKey = relatedData.foreignKey ? relatedData.foreignKey : _inflection2.default.singularize(relatedData.parentTableName) + '_' + relatedData.parentIdAttribute; if (relatedData.type === 'hasOne' || relatedData.type === 'hasMany') { qb.leftOuterJoin(relatedData.targetTableName + ' as ' + relationKey, parentKey + '.' + relatedData.parentIdAttribute, relationKey + '.' + foreignKey); } else if (relatedData.type === 'belongsTo') { if (relatedData.throughTableName) { var throughTableAlias = (relationKey + '_' + relatedData.throughTableName + '_pivot').replace(/\./g, '_'); qb.leftOuterJoin(relatedData.throughTableName + ' as ' + throughTableAlias, parentKey + '.' + relatedData.parentIdAttribute, throughTableAlias + '.' + relatedData.throughIdAttribute); qb.leftOuterJoin(relatedData.targetTableName + ' as ' + relationKey, throughTableAlias + '.' + foreignKey, relationKey + '.' + relatedData.targetIdAttribute); } else { qb.leftOuterJoin(relatedData.targetTableName + ' as ' + relationKey, parentKey + '.' + foreignKey, relationKey + '.' + relatedData.targetIdAttribute); } } else if (relatedData.type === 'belongsToMany') { var otherKey = relatedData.otherKey ? relatedData.otherKey : _inflection2.default.singularize(relatedData.targetTableName) + '_id'; var joinTableName = relatedData.joinTableName ? relatedData.joinTableName : relatedData.throughTableName; var joinTableAlias = (relationKey + '_' + joinTableName).replace(/\./g, '_'); qb.leftOuterJoin(joinTableName + ' as ' + joinTableAlias, parentKey + '.' + relatedData.parentIdAttribute, joinTableAlias + '.' + foreignKey); qb.leftOuterJoin(relatedData.targetTableName + ' as ' + relationKey, joinTableAlias + '.' + otherKey, relationKey + '.' + relatedData.targetIdAttribute); } else if ((0, _lodash.includes)(relatedData.type, 'morph')) { // Get the morph type and id var morphType = relatedData.columnNames[0] ? relatedData.columnNames[0] : relatedData.morphName + '_type'; var morphId = relatedData.columnNames[1] ? relatedData.columnNames[0] : relatedData.morphName + '_id'; if (relatedData.type === 'morphOne' || relatedData.type === 'morphMany') { qb.leftOuterJoin(relatedData.targetTableName + ' as ' + relationKey, function (qbJoin) { qbJoin.on(relationKey + '.' + morphId, '=', parentKey + '.' + relatedData.parentIdAttribute); }).where(relationKey + '.' + morphType, '=', relatedData.morphValue); } else if (relatedData.type === 'morphTo') { // Not implemented } } }); if (!(0, _lodash.keys)(relation).length) { return; } (0, _lodash.forIn)(relation, function (value, key) { return internals.queryRelations(value, key, parentModel[relationKey]().relatedData.target.forge(), relationKey); }); }; /** * Adds relations included in the column to the relationHash, used in buildDependencies * @param column {string} * @param relationHash {object} */ internals.buildDependenciesHelper = function (column, relationHash) { // Split column on colons, example of column: 'relation.column:jsonbColumn.property:dataType' var _column$split = column.split(':'),_column$split2 = _slicedToArray(_column$split, 1),key = _column$split2[0]; if ((0, _lodash.includes)(key, '.')) { // The last item in the chain is a column name, not a table. Do not include column name in relationHash key = key.substring(0, key.lastIndexOf('.')); if (!(0, _lodash.has)(relationHash, key)) { var level = relationHash; var relations = key.split('.'); var relationModel = _this.clone(); // Traverse the relationHash object and set new relation if it does not exist (0, _lodash.forEach)(relations, function (relation) { // Check if valid relationship if (typeof relationModel[relation] === 'function' && relationModel[relation]().relatedData.type) { if (!level[relation]) { level[relation] = {}; } level = level[relation]; // Set relation model to the next item in the chain relationModel = relationModel.related(relation).relatedData.target.forge(); } else { return false; } }); } } }; /** * Build a query based on the `fields` parameter. * @param fieldNames {object} */ internals.buildFields = function () {var fieldNames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};var expandedIncludes = arguments[1];var includesMap = arguments[2]; if ((0, _lodash.isObject)(fieldNames) && !(0, _lodash.isEmpty)(fieldNames)) { // Format column names fieldNames = internals.formatColumnNames(fieldNames); // Process fields for each type/relation (0, _lodash.forEach)(fieldNames, function (fieldValue, fieldKey) { // Add qualifying table name to avoid ambiguous columns fieldNames[fieldKey] = (0, _lodash.map)(fieldNames[fieldKey], function (value) { // Extract any aggregate function around the column name var _value$split = value.split(':'),_value$split2 = _slicedToArray(_value$split, 3),column = _value$split2[0],jsonColumn = _value$split2[1],dataType = _value$split2[2]; var aggregateFunction = null; var match = aggregateFunctionRegex.exec(value); if (match) { aggregateFunction = match[1]; column = match[2]; } if (!fieldKey || fieldKey === internals.modelType) { if (!(0, _lodash.includes)(column, '.')) { column = internals.modelName + '.' + column; } } else { column = fieldKey + '.' + column; } column = (0, _lodash.filter)([column, jsonColumn, dataType]).join(':'); return aggregateFunction ? { aggregateFunction: aggregateFunction, column: column } : column; }); // Only process the field if it's not a relation. Fields // for relations are processed in `buildIncludes()` if (!(0, _lodash.includes)(expandedIncludes, fieldKey)) { // Add columns to query internals.model.query(function (qb) { if (!fieldKey) { qb.distinct(); } var fieldsToSelect = fieldNames[fieldKey]; // JSON API considers relationships as fields, so we // need to make sure the id of the relation is selected if (!(0, _lodash.isEmpty)(includesMap.relations)) { fieldsToSelect = (0, _lodash.uniq)((0, _lodash.union)(fieldsToSelect, includesMap.requiredColumns.map(function (column) {return internals.modelName + '.' + column;}))); } (0, _lodash.forEach)(fieldsToSelect, function (column) { if (column.aggregateFunction) { qb[column.aggregateFunction](column.column + ' as ' + column.aggregateFunction); } else {var _column$split3 = column.split(':'),_column$split4 = _slicedToArray(_column$split3, 3),columnToSelect = _column$split4[0],jsonColumn = _column$split4[1],dataType = _column$split4[2]; if (jsonColumn) { _jsonFields2.default.buildSelect(qb, Bookshelf.knex, columnToSelect, jsonColumn, dataType); } else { qb.select([columnToSelect]); } } }); }); } }); } }; /** * Build a query based on the `filters` parameter. * @param filterValues {object|array} */ internals.buildFilters = function (filterValues) { if ((0, _lodash.isObjectLike)(filterValues) && !(0, _lodash.isEmpty)(filterValues)) { // format the column names of the filters // filterValues = this.format(filterValues); // build the filter query internals.model.query(internals.applyFilterFragment(filterValues)); } }; /** * Takes in filterValues (fragment of filter configuration) and returns function to create filter * @param filterValues {object|array} fragment of filter params */ internals.applyFilterFragment = function (filterValues) { return function (qb) { (0, _lodash.forEach)(filterValues, function (value, key) { // If the value is a filter type if ((0, _lodash.isObjectLike)(value) && !(0, _lodash.isArray)(value)) { // Format column names of filter types var filterTypeValues = value; // Check if filter type is valid if ((0, _lodash.includes)(filterTypes, key)) { // Loop through each value for the valid filter type (0, _lodash.forEach)(filterTypeValues, function (typeValue, typeKey) {var _typeKey$split = typeKey.split(':'),_typeKey$split2 = _slicedToArray(_typeKey$split, 3),column = _typeKey$split2[0],jsonColumn = _typeKey$split2[1],dataType = _typeKey$split2[2]; // Remove all but the last table name, need to get number of dots column = internals.formatRelation(internals.formatColumnNames([column])[0]); var valueArray = typeValue; if (!(0, _lodash.isArray)(valueArray)) { if (valueArray === null) { valueArray = [valueArray]; } else { valueArray = (0, _splitString2.default)(String(valueArray), { keepQuotes: true, sep: ',' }); } } if (jsonColumn) { // Pass in the an equality filter for the same column name as last parameter for OR filtering with `like` and `equals` var extraEqualityFilter = filterValues[typeKey]; if (extraEqualityFilter) { if (!(0, _lodash.isArray)(extraEqualityFilter)) { extraEqualityFilter = (0, _splitString2.default)(String(extraEqualityFilter), { keepQuotes: true, sep: ',' }); } } var filterObj = { nullString: options.nullString, qb: qb, knex: Bookshelf.knex, filterType: key, values: valueArray, column: column, jsonColumn: jsonColumn, dataType: dataType, extraEqualityFilterValues: extraEqualityFilter }; _jsonFields2.default.buildFilterWithType(filterObj); } else { // Attach different query for each type if (key === 'like') { qb.where(function (qbWhere) { var where = 'where'; var textType = 'text'; if (internals.client === 'mysql' || internals.client === 'mssql') { textType = 'char'; } (0, _lodash.forEach)(valueArray, function (val) { var likeQuery = 'LOWER(CAST(:column: AS ' + textType + ')) like LOWER(:value)'; if (internals.client === 'pg') { likeQuery = 'CAST(:column: AS ' + textType + ') ilike :value'; } qbWhere[where](Bookshelf.knex.raw(likeQuery, { value: '%' + val + '%', column: column })); // Change to orWhere after the first where if (where === 'where') { where = 'orWhere'; } }); // If the key is in the top level filter, filter on orWhereIn if ((0, _lodash.hasIn)(filterValues, typeKey)) { // Determine if there are multiple filters to be applied var equalityValue = filterValues[typeKey]; if (!(0, _lodash.isArray)(equalityValue)) { equalityValue = (0, _splitString2.default)(String(equalityValue), { keepQuotes: true, sep: ',' }); } internals.equalityFilter(qbWhere, column, equalityValue, 'orWhere'); } }); } else if (key === 'not') { var hasNull = valueArray.length !== (0, _lodash.pull)(valueArray, null, options.nullString).length; if (hasNull) { qb.whereNotNull(column); } if (!(0, _lodash.isEmpty)(valueArray)) { qb.whereNotIn(column, valueArray); } } else if (key === 'lt') { qb.where(column, '<', typeValue); } else if (key === 'gt') { qb.where(column, '>', typeValue); } else if (key === 'lte') { qb.where(column, '<=', typeValue); } else if (key === 'gte') { qb.where(column, '>=', typeValue); } } }); } } // If key is or and value is array else if ((0, _lodash.isArray)(value) && key === 'or') { qb.where(function (orStatements) { (0, _lodash.forEach)(value, function (fragment) { orStatements.orWhere(internals.applyFilterFragment(fragment)); }); }); } // If key is and and value is array else if ((0, _lodash.isArray)(value) && key === 'and') { qb.where(function (andStatements) { (0, _lodash.forEach)(value, function (fragment) { andStatements.where(internals.applyFilterFragment(fragment)); }); }); } // If the value is an equality filter else { // If the key is in the like filter, ignore the filter if (!(0, _lodash.hasIn)(filterValues.like, key)) {var _key$split = key.split(':'),_key$split2 = _slicedToArray(_key$split, 3),column = _key$split2[0],jsonColumn = _key$split2[1],dataType = _key$split2[2]; // Remove all but the last table name, need to get number of dots column = internals.formatRelation(internals.formatColumnNames([column])[0]); if (!(0, _lodash.isArray)(value)) { if (value === null) { value = [value]; } else { value = (0, _splitString2.default)(String(value), { keepQuotes: true, sep: ',' }); } } if (jsonColumn) { var eqFilterObj = { nullString: options.nullString, qb: qb, knex: Bookshelf.knex, filterType: 'equal', values: value, column: column, jsonColumn: jsonColumn, dataType: dataType, extraEqualityFilterValues: null }; _jsonFields2.default.buildFilterWithType(eqFilterObj); } else { internals.equalityFilter(qb, column, value); } } } }); }; }; /** * Takes in value, query builder, and column name for creating an equality filter * @param value {array} * @param qb {object} The query builder * @param column */ internals.equalityFilter = function (qb, column, value) {var whereType = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'where'; var hasNull = value.length !== (0, _lodash.pull)(value, options.nullString, null).length; if (hasNull) { qb[whereType](function (qbWhere) { qbWhere.whereNull(column); if (!(0, _lodash.isEmpty)(value)) { qbWhere.orWhereIn(column, value); } }); } else { qb[whereType + 'In'](column, value); } }; /** * Takes in an attribute string like a.b.c.d and returns c.d, also if attribute * looks like 'a', it will return tableName.a where tableName is the top layer table name * @param attribute {string} * @return {string} */ internals.formatRelation = function (attribute) {var _attribute$split = attribute.split(':'),_attribute$split2 = _slicedToArray(_attribute$split, 3),column = _attribute$split2[0],jsonColumn = _attribute$split2[1],dataType = _attribute$split2[2]; if ((0, _lodash.includes)(column, '.')) { var splitKey = column.split('.'); column = splitKey[splitKey.length - 2] + '.' + splitKey[splitKey.length - 1]; } // Add table name to before column name if no relation to avoid ambiguous columns else { column = internals.modelName + '.' + column; } return (0, _lodash.filter)([column, jsonColumn, dataType]).join(':'); }; /** * Takes an array from attributes and returns the only the columns and removes the table names * @param attributes {array} * @return {array} */ internals.getColumnNames = function (attributes) { return (0, _lodash.map)(attributes, function (attribute) { return attribute.substr(attribute.lastIndexOf('.') + 1); }); }; /** * Build a query based on the `include` parameter. * @param includeValues {array} */ internals.buildIncludes = function (expandedIncludes, includesMap) { if ((0, _lodash.isArray)(expandedIncludes) && !(0, _lodash.isEmpty)(expandedIncludes)) { var relations = []; var includeFunctions = (0, _lodash.last)(expandedIncludes); if ((0, _lodash.isObject)(includeFunctions)) { expandedIncludes = (0, _lodash.initial)(expandedIncludes, includeFunctions); } (0, _lodash.forEach)(expandedIncludes, function (relation) { if ((0, _lodash.has)(fields, relation) || (0, _lodash.has)(includeFunctions, relation)) { var fieldNames = internals.formatColumnNames((0, _lodash.get)(fields, relation)); var relationGetter = 'relations.' + relation.split('.').join('.relations.'); var relationObject = (0, _lodash.get)(includesMap, relationGetter); relations.push(_defineProperty({}, relation, function (qb) { if ((0, _lodash.has)(includeFunctions, relation)) { includeFunctions[relation](qb); } // Fetch existing columns from query builder and combine them with fieldNames var selectFromQB = (0, _lodash.filter)(qb._statements, { grouping: 'columns' }); if (selectFromQB.length || fieldNames.length) { // Combine the required columns with the desired columns var requiredRelationColumns = (0, _lodash.get)(relationObject, 'requiredColumns'); var columnsToSelect = (0, _lodash.uniq)(_lodash.union.apply(undefined, _toConsumableArray((0, _lodash.map)(selectFromQB, 'value')).concat([fieldNames, requiredRelationColumns]))); // Clear existing columns qb.clearSelect(); // Put the table name in front of each column in cases where there are joins in the subquery columnsToSelect = (0, _lodash.map)(columnsToSelect, function (fieldName) { // If there is already an existing table name in the query, do not replace it if ((0, _lodash.includes)(fieldName, '.')) { return fieldName; } return relationObject.tableName + '.' + fieldName; }); // Select the columns if (columnsToSelect.length) { if (!(0, _lodash.has)(includeFunctions, relation)) { qb.distinct(); } qb.columns(columnsToSelect); } } })); } else { relations.push(relation); } }); // Assign the relations to the options passed to fetch/All (0, _lodash.assign)(opts, { withRelated: relations }); } }; /* * * Used for generating columns to automatically include on a relationship * @returns {Object} Object nested map of included relationships, where each relationship has all required columns for mapping relationships to their parents * * example: * { * model: personModel, * tableName: 'person', * requiredColumns: ['id'], * relations: { * pet: { * model: petModel, * tableName: 'pet', * requiredColumns: ['id', 'pet_owner_id'], // id is required for the toy relationship * relations: { * toy: { * model: toyModel, * tableName: 'toy', * requiredColumns: ['id', 'pet_id'] * } * } * } * } * } */ internals.setupIncludesMap = function (expandedIncludes) { var includesMap = { model: internals.model, requiredColumns: [internals.model.idAttribute], relations: {} }; // expandedIncludes is ordered by the lowest amount of '.' characters // Each item in expandedIncludes can be assumed to already be in the includes map by the time the forEach iteration reaches that item (0, _lodash.forEach)(expandedIncludes, function (includeValue) { // Ignore the value if it's not a string, there is another string to represent the includes that are functions if ((0, _lodash.isObject)(includeValue)) { return; } var splitIncludeValue = includeValue.split('.'); // Create a getter/setter string // Getter string for 'a.b.c' will look like 'relations.a.relations.b.relations.c var getterString = 'relations.' + splitIncludeValue.join('.relations.'); // The parent model object, the one before this new relation var parent = includesMap; // If this relation is not on the first level, get the parent if (splitIncludeValue.length > 1) { parent = (0, _lodash.get)(includesMap, 'relations.' + (0, _lodash.initial)(splitIncludeValue).join('.relations.')); } // Get the model and relationship var relationName = (0, _lodash.last)(splitIncludeValue); var relatedData