forest-express-sequelize
Version:
Official Express/Sequelize Liana for Forest
302 lines (283 loc) • 11.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.string.trim.js");
var _forestExpress = require("forest-express");
var _lodash = _interopRequireDefault(require("lodash"));
var _database = require("../utils/database");
var _operators = _interopRequireDefault(require("../utils/operators"));
var _query = _interopRequireDefault(require("../utils/query"));
var _sequelizeCompatibility = _interopRequireDefault(require("../utils/sequelize-compatibility"));
var _errors = require("./errors");
var _filtersParser = _interopRequireDefault(require("./filters-parser"));
var _primaryKeysManager = _interopRequireDefault(require("./primary-keys-manager"));
var _queryBuilder = _interopRequireDefault(require("./query-builder"));
var _searchBuilder = _interopRequireDefault(require("./search-builder"));
/**
* Sequelize query options generator which is configured using forest admin concepts (filters,
* search, segments, ...).
* Those can be used for update, findAll, destroy, ...
*/
class QueryOptions {
/**
* Query options which can be used with sequelize.
* i.e: Books.findAll(queryOptions.sequelizeOptions);
*/
get sequelizeOptions() {
const options = {};
if (this._sequelizeWhere) options.where = this._sequelizeWhere;
if (this._sequelizeInclude) options.include = this._sequelizeInclude;
if (this._sequelizeOrder.length) options.order = this._sequelizeOrder;
if (this._offset !== undefined && this._limit !== undefined) {
options.offset = this._offset;
options.limit = this._limit;
}
if (this._requestedFields.size && !this._hasRequestedSmartFields) {
// Restricting loaded fields on the root model is opt-in with sequelize to avoid
// side-effects as this was not supported historically and it would probably break
// smart fields.
// @see https://github.com/ForestAdmin/forest-express-sequelize/blob/7d7ad0/src/services/resources-getter.js#L142
const simpleSchemaFields = this._schema.fields.filter(function (field) {
return !field.reference;
}).map(function (field) {
return field.field;
});
options.attributes = [...this._requestedFields].filter(function (field) {
return simpleSchemaFields.includes(field);
});
options.attributes.push(...this._schema.primaryKeys);
}
return _sequelizeCompatibility.default.postProcess(this._model, options);
}
/**
* Used to support segments defined as a sequelize scope.
* This feature is _not_ in the documentation, but support should be kept.
*/
get sequelizeScopes() {
return this._scopes;
}
/** Compute sequelize query `.where` property */
get _sequelizeWhere() {
const operators = _operators.default.getInstance({
Sequelize: this._Sequelize
});
switch (this._where.length) {
case 0:
return null;
case 1:
return this._where[0];
default:
return _query.default.mergeWhere(operators, ...this._where);
}
}
/** Compute sequelize query `.include` property */
get _sequelizeInclude() {
const fields = [...this._requestedFields, ...this._requestedRelations, ...this._neededFields];
const include = [...new _queryBuilder.default().getIncludes(this._model, fields), ...this._customerIncludes];
return include.length ? include : null;
}
/** Compute sequelize query `.order` property */
get _sequelizeOrder() {
if ((0, _database.isMSSQL)(this._model.sequelize)) {
// Work around sequelize bug: https://github.com/sequelize/sequelize/issues/11258
const primaryKeys = Object.keys(this._model.primaryKeys);
return this._order.filter(function (order) {
return !primaryKeys.includes(order[0]);
});
}
return this._order;
}
get _hasRequestedSmartFields() {
var _this = this;
return this._schema.fields.some(function (field) {
return field.isVirtual && _this._requestedFields.has(field.field);
});
}
/**
* @param {sequelize.model} model Sequelize model that should be targeted
* @param {boolean} options.includeRelations Include BelongsTo and HasOne relations by default
* @param {boolean} options.tableAlias Alias that will be used for this table on the final query
* This should have been handled by sequelize but it was needed in order to use
* sequelize.fn('lower', sequelize.col('<alias>.col')) in the search-builder with
* has-many-getter.
*/
constructor(model, options = {}) {
var _this2 = this;
this._Sequelize = model.sequelize.constructor;
this._schema = _forestExpress.Schemas.schemas[model.name];
this._model = model.unscoped();
this._options = options;
// Used to compute relations that will go in the final 'include'
this._requestedFields = new Set();
this._requestedRelations = new Set();
this._neededFields = new Set();
this._scopes = []; // @see sequelizeScopes getter
// Other sequelize params
this._where = [];
this._order = [];
this._offset = undefined;
this._limit = undefined;
this._customerIncludes = [];
if (this._options.includeRelations) {
_lodash.default.values(this._model.associations).filter(function (association) {
return ['HasOne', 'BelongsTo'].includes(association.associationType);
}).forEach(function (association) {
return _this2._requestedRelations.add(association.associationAccessor);
});
}
}
/**
* Add the required includes from a list of field names.
* @param {string[]} fields Fields of HasOne and BelongTo relations are
* accepted (ie. 'book.name').
* @param {string[]} fields the output of the extractRequestedFields() util function
* @param {boolean} applyOnRootModel restrict fetched fields also on the root
*/
async requireFields(fields) {
var _this3 = this;
if (fields) {
fields.forEach(function (field) {
return _this3._requestedFields.add(field);
});
}
}
/**
* Filter resulting query set with packed primary ids.
* This works both for normal collection, and those which use composite primary keys.
* @param {string[]} recordIds Packed record ids
*/
async filterByIds(recordIds) {
this._where.push(new _primaryKeysManager.default(this._model).getRecordsConditions(recordIds));
}
/**
* Apply condition tree to those query options (scopes, user filters, charts, ...)
* @param {*} filters standard forest filters
* @param {string} timezone timezone of the user (required if filtering on dates)
*/
async filterByConditionTree(filters, timezone) {
var _this4 = this;
if (!filters) return;
const filterParser = new _filtersParser.default(this._schema, timezone, {
Sequelize: this._Sequelize
});
const whereClause = await filterParser.perform(filters);
this._where.push(whereClause);
const associations = await filterParser.getAssociations(filters);
associations.forEach(function (association) {
return _this4._neededFields.add(association);
});
}
/**
* Retrict rows to those matching a search string
* @param {string} search search string
* @param {boolean} searchExtended if truthy, enable search in relations
*/
async search(search, searchExtended) {
if (!search) return [];
const options = {
Sequelize: this._Sequelize
};
const fieldNames = this._requestedFields.size ? [...this._requestedFields] : null;
const helper = new _searchBuilder.default(this._model, options, {
search,
searchExtended
}, fieldNames);
const {
conditions,
include
} = await helper.performWithSmartFields(this._options.tableAlias);
if (conditions) {
this._where.push(conditions);
} else {
this._where.push(this._Sequelize.literal('(0=1)'));
}
if (include) {
if (Array.isArray(include)) {
this._customerIncludes.push(...include);
} else {
this._customerIncludes.push(include);
}
}
return helper.getFieldsSearched();
}
/**
* Apply a forestadmin segment
* @param {string} name name of the segment (from the querystring)
* @param {string} segmentQuery SQL query of the segment (also from querystring)
*/
async segment(name) {
var _this$_schema$segment;
if (!name) return;
const segment = (_this$_schema$segment = this._schema.segments) === null || _this$_schema$segment === void 0 ? void 0 : _this$_schema$segment.find(function (s) {
return s.name === name;
});
// Segments can be provided as a sequelize scope (undocumented).
if (segment !== null && segment !== void 0 && segment.scope) {
this._scopes.push(segment.scope);
}
// ... or as a function which returns a sequelize where clause ...
if (typeof (segment === null || segment === void 0 ? void 0 : segment.where) === 'function') {
this._where.push(await segment.where());
}
}
/**
* Apply a segment query.
* FIXME: Select SQL injection allows to fetch any information from database.
* @param {string} query
*/
async segmentQuery(query) {
if (!query) return;
const primaryKey = _lodash.default.values(this._model.primaryKeys)[0].field;
const queryToFilterRecords = query.trim();
try {
const options = {
type: this._Sequelize.QueryTypes.SELECT
};
const records = await this._model.sequelize.query(queryToFilterRecords, options);
const recordIds = records.map(function (result) {
return result[primaryKey] || result.id;
});
this.filterByIds(recordIds);
} catch (error) {
const errorMessage = `Invalid SQL query for this Live Query segment:\n${error.message}`;
_forestExpress.logger.error(errorMessage);
throw new _errors.ErrorHTTP422(errorMessage);
}
}
/**
* Apply sort instructions from a sort string in the form 'field', '-field' or 'field.subfield'.
* Multiple sorts are not supported
* @param {string} sortString a sort string
*/
async sort(sortString) {
if (!sortString) return;
const [sortField, order] = sortString[0] === '-' ? [sortString.substring(1), 'DESC'] : [sortString, 'ASC'];
if (sortField.includes('.')) {
// Sort on the belongsTo displayed field
const [associationName, fieldName] = sortField.split('.');
this._order.push([associationName, fieldName, order]);
this._neededFields.add(sortField);
} else {
this._order.push([sortField, order]);
}
}
/**
* Apply pagination.
* When called with invalid parameters the query will be paginated using default values.
* @param {number|string} number page number (starting at one)
* @param {number|string} size page size
*/
async paginate(number, size) {
const limit = Number.parseInt(size, 10);
const offset = (Number.parseInt(number, 10) - 1) * limit;
if (offset >= 0 && limit > 0) {
this._offset = offset;
this._limit = limit;
} else {
this._offset = 0;
this._limit = 10;
}
}
}
module.exports = QueryOptions;