UNPKG

forest-express-sequelize

Version:

Official Express/Sequelize Liana for Forest

249 lines (245 loc) 10.3 kB
"use strict"; require("core-js/modules/es.array.iterator.js"); require("core-js/modules/es.regexp.exec.js"); require("core-js/modules/es.promise.js"); const _ = require('lodash'); const Interface = require('forest-express'); const Operators = require('../utils/operators'); const Database = require('../utils/database'); const { isUUID } = require('../utils/orm'); const REGEX_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function SearchBuilder(model, opts, params, fieldNamesRequested) { var _this = this; const schema = Interface.Schemas.schemas[model.name]; const DataTypes = opts.Sequelize; const fields = _.clone(schema.fields); let associations = _.clone(model.associations); const hasSearchFields = schema.searchFields && _.isArray(schema.searchFields); let searchAssociationFields; const OPERATORS = Operators.getInstance(opts); const fieldsSearched = []; let hasExtendedConditions = false; function lowerIfNecessary(entry) { // NOTICE: MSSQL search is natively case insensitive, do not use the "lower" function for // performance optimization. if (Database.isMSSQL(model.sequelize)) { return entry; } return opts.Sequelize.fn('lower', entry); } function selectSearchFields() { const searchFields = _.clone(schema.searchFields); searchAssociationFields = _.remove(searchFields, function (field) { return field.indexOf('.') !== -1; }); _.remove(fields, function (field) { return !_.includes(schema.searchFields, field.field); }); const searchAssociationNames = _.map(searchAssociationFields, function (association) { return association.split('.')[0]; }); associations = _.pick(associations, searchAssociationNames); // NOTICE: Compute warnings to help developers to configure the // searchFields. const fieldsSimpleNotFound = _.xor(searchFields, _.map(fields, function (field) { return field.field; })); const fieldsAssociationNotFound = _.xor(_.map(searchAssociationFields, function (association) { return association.split('.')[0]; }), _.keys(associations)); if (fieldsSimpleNotFound.length) { Interface.logger.warn(`Cannot find the fields [${fieldsSimpleNotFound}] while searching records in model ${model.name}.`); } if (fieldsAssociationNotFound.length) { Interface.logger.warn(`Cannot find the associations [${fieldsAssociationNotFound}] while searching records in model ${model.name}.`); } } function getStringExtendedCondition(attributes, value, column) { if (attributes && isUUID(DataTypes, attributes.type)) { if (!value.match(REGEX_UUID)) { return null; } return opts.Sequelize.where(column, '=', value); } return opts.Sequelize.where(lowerIfNecessary(column), ' LIKE ', lowerIfNecessary(`%${value}%`)); } this.getFieldsSearched = function () { return fieldsSearched; }; this.hasExtendedSearchConditions = function () { return hasExtendedConditions; }; this.performWithSmartFields = async function (associationName) { // Retrocompatibility: customers which implement search on smart fields are expected to // inject their conditions at .where[Op.and][0][Op.or].push(searchCondition) // https://docs.forestadmin.com/documentation/reference-guide/fields/create-and-manage-smart-fields const query = { include: [], where: { [OPERATORS.AND]: [_this.perform(associationName)] } }; const fieldsWithSearch = schema.fields.filter(function (field) { return field.search; }); await Promise.all(fieldsWithSearch.map(async function (field) { try { await field.search(query, params.search); } catch (error) { const errorMessage = `Cannot search properly on Smart Field ${field.field}: `; Interface.logger.error(errorMessage, error); } return Promise.resolve(); })); return { include: query.include, conditions: query.where[OPERATORS.AND][0][OPERATORS.OR].length ? query.where[OPERATORS.AND][0] : null }; }; this.perform = function (associationName) { if (!params.search) { return null; } if (hasSearchFields) { selectSearchFields(); } const aliasName = associationName || schema.name; const or = []; function pushCondition(condition, fieldName) { or.push(condition); fieldsSearched.push(fieldName); } function getConditionValueForNumber(search, keyType) { const searchAsNumber = Number(search); if (Number.isNaN(searchAsNumber)) { return null; } if (Number.isSafeInteger(searchAsNumber) || !Number.isInteger(searchAsNumber)) { return searchAsNumber; } // Integers higher than MAX_SAFE_INTEGER need to be handled as strings to circumvent // precision problems only if the field type is a big int. if (keyType instanceof DataTypes.BIGINT) { return search; } return null; } _.each(fields, function (field) { if (field.isVirtual) { return; } // NOTICE: Ignore Smart Fields. if (field.integration) { return; } // NOTICE: Ignore integration fields. if (field.reference) { return; } // NOTICE: Handle belongsTo search below. let condition = {}; let columnName; if (field.field === schema.idField) { const primaryKeyType = model.primaryKeys[schema.idField].type; if (primaryKeyType instanceof DataTypes.INTEGER) { const conditionValue = getConditionValueForNumber(params.search, primaryKeyType); if (conditionValue !== null) { condition[field.field] = conditionValue; pushCondition(condition, field.field); } } else if (primaryKeyType instanceof DataTypes.STRING) { columnName = field.columnName || field.field; condition = opts.Sequelize.where(lowerIfNecessary(opts.Sequelize.col(`${aliasName}.${columnName}`)), ' LIKE ', lowerIfNecessary(`%${params.search}%`)); pushCondition(condition, field.field); } else if (isUUID(DataTypes, primaryKeyType) && params.search.match(REGEX_UUID)) { condition[field.field] = params.search; pushCondition(condition, field.field); } } else if (field.type === 'Enum') { let enumValueFound; const searchValue = params.search.toLowerCase(); _.each(field.enums, function (enumValue) { if (enumValue.toLowerCase() === searchValue) { enumValueFound = enumValue; } }); if (enumValueFound) { condition[field.field] = enumValueFound; pushCondition(condition, field.field); } } else if (field.type === 'String') { if (model.rawAttributes[field.field] && isUUID(DataTypes, model.rawAttributes[field.field].type)) { if (params.search.match(REGEX_UUID)) { condition[field.field] = params.search; pushCondition(condition, field.field); } } else { columnName = field.columnName || field.field; condition = opts.Sequelize.where(lowerIfNecessary(opts.Sequelize.col(`${aliasName}.${columnName}`)), ' LIKE ', lowerIfNecessary(`%${params.search}%`)); pushCondition(condition, field.field); } } else if (field.type === 'Number') { const conditionValue = getConditionValueForNumber(params.search, model.rawAttributes[field.field].type); if (conditionValue !== null) { condition[field.field] = conditionValue; pushCondition(condition, field.field); } } }); // NOTICE: Handle search on displayed belongsTo if (parseInt(params.searchExtended, 10)) { _.each(associations, function (association) { if (!fieldNamesRequested || fieldNamesRequested.includes(association.as) || fieldNamesRequested.find(function (fieldName) { return fieldName.startsWith(`${association.as}.`); })) { if (['HasOne', 'BelongsTo'].indexOf(association.associationType) > -1) { const modelAssociation = association.target; const schemaAssociation = Interface.Schemas.schemas[modelAssociation.name]; const fieldsAssociation = schemaAssociation.fields; _.each(fieldsAssociation, function (field) { if (field.isVirtual) { return; } // NOTICE: Ignore Smart Fields. if (field.integration) { return; } // NOTICE: Ignore integration fields. if (field.reference) { return; } // NOTICE: Ignore associations. if (field.isSearchable === false) { return; } if (hasSearchFields && !_.includes(searchAssociationFields, `${association.as}.${field.field}`)) { return; } let condition = null; const columnName = field.columnName || field.field; const column = opts.Sequelize.col(`${association.as}.${columnName}`); if (field.field === schemaAssociation.idField) { if (field.type === 'Number') { const value = parseInt(params.search, 10) || 0; if (value) { condition = opts.Sequelize.where(column, ' = ', value); hasExtendedConditions = true; } } else if (field.type === 'String') { condition = getStringExtendedCondition(modelAssociation.rawAttributes[field.field], params.search, column); hasExtendedConditions = !!condition; } } else if (field.type === 'String') { condition = getStringExtendedCondition(modelAssociation.rawAttributes[field.field], params.search, column); hasExtendedConditions = !!condition; } if (condition) { or.push(condition); } }); } } }); } return { [OPERATORS.OR]: or }; }; } module.exports = SearchBuilder;