forest-express-sequelize
Version:
Official Express/Sequelize Liana for Forest
152 lines (146 loc) • 7.21 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
require("core-js/modules/es.array.sort.js");
require("core-js/modules/es.array.iterator.js");
var _objectTools = _interopRequireDefault(require("./object-tools"));
var _operators = _interopRequireDefault(require("./operators"));
var _query = _interopRequireDefault(require("./query"));
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* Extract all where conditions along the include tree, and bubbles them up to the top in-place.
* This allows to work around a sequelize quirk that cause nested 'where' to fail when they
* refer to relation fields from an intermediary include (ie '$book.id$').
*
* This happens when forest admin filters on relations are used.
*
* @see https://sequelize.org/master/manual/eager-loading.html#complex-where-clauses-at-the-top-level
* @see https://github.com/ForestAdmin/forest-express-sequelize/blob/7d7ad0/src/services/filters-parser.js#L104
*/
function bubbleWheresInPlace(operators, options) {
var _options$include;
const parentIncludeList = (_options$include = options.include) !== null && _options$include !== void 0 ? _options$include : [];
parentIncludeList.forEach(function (include) {
bubbleWheresInPlace(operators, include);
if (include.where) {
const newWhere = _objectTools.default.mapKeysDeep(include.where, function (key) {
var _include$model$rawAtt, _include$model, _include$model$rawAtt2, _include$model$rawAtt3;
// Targeting a nested field, simply nest it deeper.
if (key[0] === '$' && key[key.length - 1] === '$') {
return `$${include.as}.${key.substring(1)}`;
}
// Targeting a simple field.
// Try to resolve the column name, as sequelize does not allow using model aliases here.
return `$${include.as}.${(_include$model$rawAtt = (_include$model = include.model) === null || _include$model === void 0 ? void 0 : (_include$model$rawAtt2 = _include$model.rawAttributes) === null || _include$model$rawAtt2 === void 0 ? void 0 : (_include$model$rawAtt3 = _include$model$rawAtt2[key]) === null || _include$model$rawAtt3 === void 0 ? void 0 : _include$model$rawAtt3.field) !== null && _include$model$rawAtt !== void 0 ? _include$model$rawAtt : key}$`;
});
options.where = _query.default.mergeWhere(operators, options.where, newWhere);
delete include.where;
}
});
}
/**
* Includes can be expressed in different ways in sequelize, which is inconvenient to
* remove duplicate associations.
* This convert all valid ways to perform eager loading into [{model: X, as: 'x'}].
*
* This is necessary as we have no control over which way customer use when writing SmartFields
* search handlers.
*
* Among those:
* - { include: [Book] }
* - { include: [{ association: 'book' }] }
* - { include: ['book'] }
* - { include: [[{ as: 'book' }]] }
* - { include: [[{ model: Book }]] }
*/
function normalizeInclude(model, include) {
if (include.sequelize) {
return {
model: include,
as: Object.keys(model.associations).find(function (association) {
return model.associations[association].target.name === include.name;
})
};
}
if (typeof include === 'string' && model.associations[include]) {
return {
as: include,
model: model.associations[include].target
};
}
if (typeof include === 'object') {
if (typeof include.association === 'string' && model.associations[include.association]) {
include.as = include.association;
delete include.association;
}
if (typeof include.as === 'string' && !include.model && model.associations[include.as]) {
const includeModel = model.associations[include.as].target;
include.model = includeModel;
}
if (include.model && !include.as) {
include.as = Object.keys(model.associations).find(function (association) {
return model.associations[association].target.name === include.model.name;
});
}
}
// Recurse
if (include.include) {
if (Array.isArray(include.include)) {
include.include = include.include.map(function (childInclude) {
return normalizeInclude(include.model, childInclude);
});
} else {
include.include = [normalizeInclude(include.model, include.include)];
}
}
return include;
}
/**
* Remove duplications in a queryOption.include array in-place.
* Using multiple times the same association yields invalid SQL when using sequelize <= 4.x
*/
function removeDuplicateAssociations(model, includeList) {
// Remove duplicates
includeList.sort(function (include1, include2) {
return include1.as < include2.as ? -1 : 1;
});
for (let i = 1; i < includeList.length; i += 1) {
if (includeList[i - 1].as === includeList[i].as) {
const newInclude = _objectSpread(_objectSpread({}, includeList[i - 1]), includeList[i]);
if (includeList[i - 1].attributes && includeList[i].attributes) {
// Keep 'attributes' only when defined on both sides.
newInclude.attributes = [...new Set([...includeList[i - 1].attributes, ...includeList[i].attributes])].sort();
} else {
delete newInclude.attributes;
}
if (includeList[i - 1].include || includeList[i].include) {
var _includeList$include, _includeList$i$includ;
newInclude.include = [...((_includeList$include = includeList[i - 1].include) !== null && _includeList$include !== void 0 ? _includeList$include : []), ...((_includeList$i$includ = includeList[i].include) !== null && _includeList$i$includ !== void 0 ? _includeList$i$includ : [])];
}
includeList[i - 1] = newInclude;
includeList.splice(i, 1);
i -= 1;
}
}
// Recurse
includeList.forEach(function (include) {
const association = model.associations[include.as];
if (include.include && association) {
removeDuplicateAssociations(association.target, include.include);
}
});
}
exports.postProcess = function (model, rawOptions) {
if (!rawOptions.include) return rawOptions;
const options = rawOptions;
const operators = _operators.default.getInstance({
Sequelize: model.sequelize.constructor
});
options.include = options.include.map(function (include) {
return normalizeInclude(model, include);
});
bubbleWheresInPlace(operators, options);
removeDuplicateAssociations(model, options.include);
return options;
};
;