jii
Version:
Jii - Full-Stack JavaScript Framework
416 lines (352 loc) • 14.1 kB
JavaScript
/**
* @author <a href="http://www.affka.ru">Vladimir Kozhin</a>
* @license MIT
*/
'use strict';
const Jii = require('../BaseJii');
const NotSupportedException = require('../exceptions/NotSupportedException');
const InvalidParamException = require('../exceptions/InvalidParamException');
const _isEmpty = require('lodash/isEmpty');
const _isArray = require('lodash/isArray');
const _isObject = require('lodash/isObject');
const _uniq = require('lodash/uniq');
const _every = require('lodash/every');
const _keys = require('lodash/keys');
const _each = require('lodash/each');
const _some = require('lodash/some');
const _has = require('lodash/has');
const BaseObject = require('../base/BaseObject');
const Query = require('../data/Query');
const Expression = require('../data/Expression');
const Model = require('../base/Model');
class FilterBuilder extends BaseObject {
preInit() {
/**
* @var array map of query condition to builder methods.
* These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
*/
this._conditionBuilders = {
'NOT': [
'filterNotCondition',
'attributesNotCondition'
],
'AND': [
'filterAndCondition',
'attributesAndCondition'
],
'OR': [
'filterAndCondition',
'attributesAndCondition'
],
'BETWEEN': [
'filterBetweenCondition',
'attributesBetweenCondition'
],
'IN': [
'filterInCondition',
'attributesInCondition'
],
'LIKE': [
'filterLikeCondition',
'attributesLikeCondition'
],
'OR LIKE': [
'filterLikeCondition',
'attributesLikeCondition'
],
'EXISTS': [
'filterExistsCondition',
'attributesExistsCondition'
]
};
super.preInit(...arguments);
}
prepare(query) {
// @todo prepare, tmp
const ActiveQuery = require('../data/ActiveQuery');
if (query instanceof ActiveQuery) {
query._filterByModels([query.primaryModel]);
}
}
filter(row, query) {
return this.filterCondition(row, query.getWhere());
}
attributes(query) {
return _uniq(this.attributesCondition(query.getWhere()));
}
createFilter(query) {
return row => {
if (row instanceof Model) {
row = row.getAttributes();
}
return this.filter(row, query);
};
}
filterCondition(row, condition) {
if (_isEmpty(condition)) {
return true;
}
if (condition[0]) {
// operator format: operator, operand 1, operand 2, ...
condition = [].concat(condition);
var operator = condition[0].toUpperCase();
var method = this._conditionBuilders[operator] ? this._conditionBuilders[operator][0] : 'filterSimpleCondition';
if (operator.indexOf('NOT ') !== -1) {
condition[0] = condition[0].toUpperCase().replace(/NOT /, '');
return this.filterNotCondition(row, 'NOT', [condition]);
}
condition.shift();
return this[method].call(this, row, operator, condition);
} else {
// hash format: {'column1': 'value1', 'column2': 'value2', ...}
return this.filterHashCondition(row, condition);
}
}
attributesCondition(condition) {
if (_isEmpty(condition)) {
return [];
}
if (condition[0]) {
// operator format: operator, operand 1, operand 2, ...
condition = [].concat(condition);
var operator = condition[0].toUpperCase();
var method = this._conditionBuilders[operator] ? this._conditionBuilders[operator][1] : 'attributesSimpleCondition';
if (operator.indexOf('NOT ') !== -1) {
condition[0] = condition[0].toUpperCase().replace(/NOT /, '');
return this.attributesNotCondition('NOT', [condition]);
}
condition.shift();
return this[method].call(this, operator, condition);
} else {
// hash format: {'column1': 'value1', 'column2': 'value2', ...}
return this.attributesHashCondition(condition);
}
}
filterHashCondition(row, condition) {
return _every(condition, (value, column) => {
if (_isArray(value) || value instanceof Query) {
// IN condition
return this.filterInCondition(row, 'IN', [
column,
value
]);
}
// Null
if (value === null) {
return row[column] === null;
}
// Null
if (value instanceof Expression) {
// @todo implement
throw new NotSupportedException();
}
return row[column] == value;
});
}
attributesHashCondition(condition) {
return _keys(condition);
}
filterAndCondition(row, operator, operands) {
var method = operator === 'AND' ? _every : _some;
return method(operands, operand => {
if (_isArray(operand) || _isObject(operand)) {
return this.filterCondition(row, operand);
}
// @todo implement.. string?
throw new NotSupportedException();
});
}
attributesAndCondition(operator, operands) {
var attributes = [];
_each(operands, operand => {
if (_isArray(operand) || _isObject(operand)) {
attributes = attributes.concat(this.attributesCondition(operand));
} else {
// @todo implement.. string?
throw new NotSupportedException();
}
});
return attributes;
}
filterNotCondition(row, operator, operands) {
if (operands.length !== 1) {
throw new InvalidParamException('Operator `' + operator + '` requires exactly one operand.');
}
var bool = true;
if (_isArray(operands[0]) || _isObject(operands[0])) {
bool = this.filterCondition(row, operands[0]);
}
return operator === 'NOT' ? !bool : bool;
}
attributesNotCondition(operator, operands) {
return this.attributesAndCondition(operator, operands);
}
filterBetweenCondition(row, operator, operands) {
if (operands.length !== 3) {
throw new InvalidParamException('Operator `' + operator + '` requires three operands.');
}
var column = operands[0];
var value1 = operands[1];
var value2 = operands[2];
return value1 <= row[column] && row[column] <= value2;
}
attributesBetweenCondition(operator, operands) {
return operands[0];
}
filterInCondition(row, operator, operands) {
if (operands.length !== 2) {
throw new InvalidParamException('Operator `' + operator + '` requires two operands.');
}
var column = operands[0];
var values = operands[1];
if (_isEmpty(values) || _isEmpty(column)) {
return false;
}
if (values instanceof Query) {
// sub-query
throw new NotSupportedException();
}
if (!_isArray(values)) {
values = [values];
}
if (_isArray(column) && column.length > 1) {
// @todo
throw new NotSupportedException();
}
if (_isArray(column)) {
column = column[0];
}
return _some(values, value => {
if (_isObject(value)) {
value = _has(value, column) ? value[column] : null;
}
if (value === null) {
return row[column] === null;
}
if (value instanceof Expression) {
// @todo
throw new NotSupportedException();
}
return row[column] == value;
});
}
attributesInCondition(operator, operands) {
return [].concat(operands[0]);
}
/**
* Creates an SQL expressions with the `LIKE` operator.
* @param {string} operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param {[]} operands an array of two or three operands
*
* - The first operand is the column name.
* - The second operand is a single value or an array of values that column value
* should be compared with. If it is an empty array the generated expression will
* be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
* is `NOT LIKE` or `OR NOT LIKE`.
* - An optional third operand can also be provided to specify how to escape special characters
* in the value(s). The operand should be an array of mappings from the special characters to their
* escaped counterparts. If this operand is not provided, a default escape mapping will be used.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
* @param {object} params the binding parameters to be populated
* @return {string} the generated SQL expression
* @throws {InvalidParamException} if wrong number of operands have been given.
*/
filterLikeCondition(operator, operands, params) {
if (operands.length !== 2) {
throw new InvalidParamException('Operator `' + operator + '` requires two operands.');
}
var escape = operands[2] || {
'%': '\\%',
'_': '\\_',
'\\': '\\\\'
};
delete operands[2];
var matches = /^(AND |OR |)((NOT |)I?LIKE)/.exec(operator);
if (matches === null) {
throw new InvalidParamException('Invalid operator `' + operator + '`.');
}
// @todo http://stackoverflow.com/questions/1314045/emulating-sql-like-in-javascript
throw new NotSupportedException();
/*var andor = ' ' + (matches[1] || 'AND ');
var not = !!matches[3];
var parsedOperator = matches[2];
var column = operands[0];
var values = operands[1];
if (_isEmpty(values)) {
return Promise.resolve(not ? '' : '0=1');
}
if (!_isArray(values)) {
values = [values];
}
if (column.indexOf('(') === -1) {
column = this.db.quoteColumnName(column);
}
var parts = [];
_each(values, value => {
var phName = null;
if (value instanceof Expression) {
_each(value.params, (n, v) => {
params[n] = v;
});
phName = value.expression;
} else {
phName = this.__static.PARAM_PREFIX + _size(params);
if (!_isEmpty(escape)) {
_each(escape, (to, from) => {
value = value.split(from).join(to);
});
value = '%' + value + '%';
}
params[phName] = value;
}
parts.push(column + ' ' + parsedOperator + ' ' + phName);
});
return Promise.resolve(parts.join(andor));*/
}
/**
* Creates an SQL expressions with the `EXISTS` operator.
* @param {string} operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
* @param {[]} operands contains only one element which is a [[Query]] object representing the sub-query.
* @param {object} params the binding parameters to be populated
* @return {string} the generated SQL expression
* @throws {InvalidParamException} if the operand is not a [[Query]] object.
*/
filterExistsCondition(operator, operands, params) {
throw new NotSupportedException();
}
/**
* Creates an SQL expressions like `"column" operator value`.
* @param {string} operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
* @param {[]} operands contains two column names.
* @param {object} params the binding parameters to be populated
* @returns {string} the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
*/
filterSimpleCondition(operator, operands, params) {
if (operands.length !== 2) {
throw new InvalidParamException('Operator `' + operator + '` requires two operands.');
}
var column = operands[0];
var value = operands[1];
// @todo
throw new NotSupportedException();
/*
var condition = null;
if (value === null) {
condition = column + ' ' + operator + ' NULL';
} else if (value instanceof Expression) {
_each(value.params, (v, n) => {
params[n] = v;
});
condition = column + ' ' + operator + ' ' + value.expression;
} else {
var phName = this.__static.PARAM_PREFIX + _size(params);
params[phName] = value;
condition = column + ' ' + operator + ' ' + phName;
}
return Promise.resolve(condition);*/
}
}
module.exports = FilterBuilder;