From 1cf7f1fa3db399039a45fcaf7574237793394415 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 28 Jun 2018 12:28:23 +0200 Subject: [PATCH 01/35] Category filter --- src/api/v4/filter/sql/category.js | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/api/v4/filter/sql/category.js diff --git a/src/api/v4/filter/sql/category.js b/src/api/v4/filter/sql/category.js new file mode 100644 index 0000000000..0560b638a3 --- /dev/null +++ b/src/api/v4/filter/sql/category.js @@ -0,0 +1,59 @@ +const SQLBaseFilter = require('./sql-base-filter'); +const _ = require('underscore'); + +const CATEGORY_COMPARISON_OPERATORS = { + EQ: 'eq', + NOT_EQ: 'not_eq', + IN: 'in', + NOT_IN: 'not_in', + LIKE: 'like', + SIMILAR_TO: 'similar_to' +}; + +const ALLOWED_FILTERS = Object.freeze(_.values(CATEGORY_COMPARISON_OPERATORS)); + +/** + * Category Filter + * + * When including this filter into a {@link source.sql} or a {@link source.dataset}, the rows will be filtered by the conditions included within the filter. + * + * @class carto.filter.Category + * @extends carto.filter.SQLBaseFilter + * @memberof carto.filter + * @api + */ +class Category extends SQLBaseFilter { + /** + * Create a Category Filter + * @param {string} column - The column which the filter will be performed against + * @param {object} [filters] - The filters that you want to apply to the table rows + * @param {(string|number|Date)} [filters.eq] - Filter rows whose column value is equal to the provided value + * @param {(string|number|Date)} [filters.neq] - Filter rows whose column value is not equal to the provided value + * @param {string[]} [filters.in] - Filter rows whose column value is included within the provided values + * @param {string[]} [filters.not_in] - Filter rows whose column value is included within the provided values + * @param {string} [filters.like] - Filter rows whose column value is like the provided value + * @param {string} [filters.similar_to] - Filter rows whose column value is similar to the provided values + * @param {object} [options] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.reverseConditions] - The operation to apply to the data + */ + constructor (column, filters, options) { + super(column, options); + + this.ALLOWED_FILTERS = ALLOWED_FILTERS; + + this._checkFilters(filters); + this._filters = filters; + + this.SQL_TEMPLATES = { + [CATEGORY_COMPARISON_OPERATORS.IN]: '<%= column %> IN (<%= value %>)', + [CATEGORY_COMPARISON_OPERATORS.NOT_IN]: '<%= column %> NOT IN (<%= value %>)', + [CATEGORY_COMPARISON_OPERATORS.EQ]: '<%= column %> == <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.NOT_EQ]: '<%= column %> != <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.LIKE]: '<%= column %> LIKE <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.SIMILAR_TO]: '<%= column %> SIMILAR TO <%= value %>' + }; + } +} + +module.exports = Category; From f537c0d76e7bfec88f814b44baf5c0c7b14a15ad Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 28 Jun 2018 12:28:35 +0200 Subject: [PATCH 02/35] Range filter --- src/api/v4/filter/sql/range.js | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/api/v4/filter/sql/range.js diff --git a/src/api/v4/filter/sql/range.js b/src/api/v4/filter/sql/range.js new file mode 100644 index 0000000000..bffb5536a5 --- /dev/null +++ b/src/api/v4/filter/sql/range.js @@ -0,0 +1,68 @@ +const SQLBaseFilter = require('./sql-base-filter'); +const _ = require('underscore'); + +const RANGE_COMPARISON_OPERATORS = { + LT: 'lt', + LTE: 'lte', + GT: 'gt', + GTE: 'gte', + BETWEEN: 'between', + NOT_BETWEEN: 'not_between', + BETWEEN_SYMMETRIC: 'between_symmetric', + NOT_BETWEEN_SYMMETRIC: 'not_between_symmetric' +}; + +const ALLOWED_FILTERS = Object.freeze(_.values(RANGE_COMPARISON_OPERATORS)); + +/** + * Range Filter + * + * When including this filter into a {@link source.sql} or a {@link source.dataset}, the rows will be filtered by the conditions included within filters. + * + * @class carto.filter.Range + * @extends carto.filter.SQLBaseFilter + * @memberof carto.filter + * @api + */ +class Range extends SQLBaseFilter { + /** + * Create a Range filter + * + * @param {string} column - The column to filter rows + * @param {object} [filters] - The filters you want to apply to the column + * @param {(number|Date)} [filters.lt] - Filter rows whose column value is less than the provided value + * @param {(number|Date)} [filters.lte] - Filter rows whose column value is less than or equal to the provided value + * @param {(number|Date)} [filters.gt] - Filter rows whose column value is greater than to the provided value + * @param {(number|Date)} [filters.gte] - Filter rows whose column value is greater than or equal to the provided value + * @param {(number|Date)} [filters.between] - Filter rows whose column value is between the provided values + * @param {(number|Date)} [filters.between.min] - Lowest value of the comparison range + * @param {(number|Date)} [filters.between.max] - Upper value of the comparison range + * @param {(number|Date)} [filters.between_symmetric] - Filter rows whose column value is between the provided values after sorting them + * @param {(number|Date)} [filters.between_symmetric.min] - Lowest value of the comparison range + * @param {(number|Date)} [filters.between_symmetric.max] - Upper value of the comparison range + * @param {object} [options] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.reverseConditions] - The operation to apply to the data + */ + constructor (column, filters, options) { + super(column, options); + + this.ALLOWED_FILTERS = ALLOWED_FILTERS; + + this._checkFilters(filters); + this._filters = filters; + + this.SQL_TEMPLATES = { + [RANGE_COMPARISON_OPERATORS.LT]: '<%= column %> < <%= value %>', + [RANGE_COMPARISON_OPERATORS.LTE]: '<%= column %> <= <%= value %>', + [RANGE_COMPARISON_OPERATORS.GT]: '<%= column %> > <%= value %>', + [RANGE_COMPARISON_OPERATORS.GTE]: '<%= column %> >= <%= value %>', + [RANGE_COMPARISON_OPERATORS.BETWEEN]: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.NOT_BETWEEN]: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.BETWEEN_SYMMETRIC]: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.NOT_BETWEEN_SYMMETRIC]: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' + }; + } +} + +module.exports = Range; From fbc5b654e655a352022cde927bf2663641a3f852 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 28 Jun 2018 12:30:06 +0200 Subject: [PATCH 03/35] SQLFilterBase for Category and Range filters --- src/api/v4/filter/base.js | 5 + src/api/v4/filter/sql/sql-base-filter.js | 123 +++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/api/v4/filter/sql/sql-base-filter.js diff --git a/src/api/v4/filter/base.js b/src/api/v4/filter/base.js index d807d8679a..3b3ab8d5ed 100644 --- a/src/api/v4/filter/base.js +++ b/src/api/v4/filter/base.js @@ -1,5 +1,6 @@ var _ = require('underscore'); var Backbone = require('backbone'); +var CartoValidationError = require('../error-handling/carto-validation-error'); /** * Base filter object @@ -13,6 +14,10 @@ function Base () {} _.extend(Base.prototype, Backbone.Events); +Base.prototype._getValidationError = function (code) { + return new CartoValidationError('filter', code); +}; + module.exports = Base; /** diff --git a/src/api/v4/filter/sql/sql-base-filter.js b/src/api/v4/filter/sql/sql-base-filter.js new file mode 100644 index 0000000000..05f699b9bc --- /dev/null +++ b/src/api/v4/filter/sql/sql-base-filter.js @@ -0,0 +1,123 @@ +const Base = require('../base'); +const _ = require('underscore'); + +const ALLOWED_OPTIONS = ['includeNull']; +const DEFAULT_JOIN_OPERATOR = 'AND'; + +/** + * SQL Filter + * + * A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter + * + * @param {string} column - The filtering will be performed against this column + * @param {object} [filters] - The filters that you want to apply to the table rows + * @param {object} [options] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.reverseConditions] - The operation to apply to the data + * + * + * @constructor + * @extends carto.filter.Base + * @memberof carto.filter + * @api + */ +class SQLFilterBase extends Base { + constructor (column, options = {}) { + super(); + + this._checkColumn(column); + this._checkOptions(options); + + this._column = column; + this._options = options; + } + + set (filterType, filterValue) { + const filter = { [filterType]: filterValue }; + + this._checkFilters(filter); + this._filters[filterType] = filterValue; + + this.trigger('change:filters', filter); + } + + setFilters (filters) { + this._checkFilters(filters); + _.extend(this._filters, filters); + + this.trigger('change:filters', filters); + } + + _checkColumn (column) { + if (_.isUndefined(column)) { + throw this._getValidationError('columnRequired'); + } + + if (!_.isString(column)) { + throw this._getValidationError('columnString'); + } + + if (_.isEmpty(column)) { + throw this._getValidationError('emptyColumn'); + } + } + + _checkFilters (filters) { + Object.keys(filters) + .forEach(filter => { + const isFilterAllowed = _.contains(this.ALLOWED_FILTERS, filter); + + if (!isFilterAllowed) { + console.error({filter}, 'is not allowed'); + // TODO: Return a better error + throw this._getValidationError('filterNotFound'); + } + }); + } + + _checkOptions (options) { + Object.keys(options).forEach(filter => { + const isOptionAllowed = _.contains(ALLOWED_OPTIONS, filter); + + if (!isOptionAllowed) { + // TODO: Return a better error + throw this._getValidationError('optionNotFound'); + } + }); + } + + _getFilterStringValue (filterValue) { + if (_.isDate(filterValue)) { + return filterValue.toISOString(); + } + + if (_.isArray(filterValue)) { + return filterValue + .map(value => `'${value.toString()}'`) + .join(','); + } + + if (_.isNumber(filterValue)) { + return filterValue.toString(); + } + + if (_.isObject(filterValue)) { + return filterValue; + } + + return `'${filterValue.toString()}'`; + } + + _getSQLString () { + return Object.keys(this._filters) + .map(filterType => this._buildFilterString(filterType, this._filters[filterType])) + .join(` ${DEFAULT_JOIN_OPERATOR} `); + } + + _buildFilterString (filterType, filterValue) { + const sqlStringTemplate = _.template(this.SQL_TEMPLATES[filterType]); + return sqlStringTemplate({ column: this._column, value: this._getFilterStringValue(filterValue) }); + } +} + +module.exports = SQLFilterBase; From 9f2e78006fde202e55adfb7cb62fed6c4e74bd3a Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 28 Jun 2018 12:30:40 +0200 Subject: [PATCH 04/35] Export Category and Range filter to filters --- src/api/v4/filter/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/api/v4/filter/index.js b/src/api/v4/filter/index.js index f80f99fb22..5ce8b44d7e 100644 --- a/src/api/v4/filter/index.js +++ b/src/api/v4/filter/index.js @@ -1,13 +1,17 @@ -var BoundingBox = require('./bounding-box'); -var BoundingBoxLeaflet = require('./bounding-box-leaflet'); -var BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); +const BoundingBox = require('./bounding-box'); +const BoundingBoxLeaflet = require('./bounding-box-leaflet'); +const BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); +const Category = require('./sql/category'); +const Range = require('./sql/range'); /** * @namespace carto.filter * @api */ module.exports = { - BoundingBox: BoundingBox, - BoundingBoxLeaflet: BoundingBoxLeaflet, - BoundingBoxGoogleMaps: BoundingBoxGoogleMaps + BoundingBox, + BoundingBoxLeaflet, + BoundingBoxGoogleMaps, + Category, + Range }; From 7df4bb4aa548d1b1bba9670b2561205abee84349 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 28 Jun 2018 18:40:20 +0200 Subject: [PATCH 05/35] Add tests and refactor --- .../error-list/validation-errors.js | 21 +++ src/api/v4/filter/base-sql.js | 117 ++++++++++++ src/api/v4/filter/{sql => }/category.js | 33 ++-- src/api/v4/filter/index.js | 4 +- src/api/v4/filter/{sql => }/range.js | 25 +-- src/api/v4/filter/sql/sql-base-filter.js | 123 ------------ test/spec/api/v4/filter/base-sql.spec.js | 177 ++++++++++++++++++ test/spec/api/v4/filter/category.spec.js | 43 +++++ test/spec/api/v4/filter/range.spec.js | 53 ++++++ 9 files changed, 445 insertions(+), 151 deletions(-) create mode 100644 src/api/v4/filter/base-sql.js rename src/api/v4/filter/{sql => }/category.js (67%) rename src/api/v4/filter/{sql => }/range.js (78%) delete mode 100644 src/api/v4/filter/sql/sql-base-filter.js create mode 100644 test/spec/api/v4/filter/base-sql.spec.js create mode 100644 test/spec/api/v4/filter/category.spec.js create mode 100644 test/spec/api/v4/filter/range.spec.js diff --git a/src/api/v4/error-handling/error-list/validation-errors.js b/src/api/v4/error-handling/error-list/validation-errors.js index b32d5fde2c..a79dca2f20 100644 --- a/src/api/v4/error-handling/error-list/validation-errors.js +++ b/src/api/v4/error-handling/error-list/validation-errors.js @@ -189,6 +189,27 @@ module.exports = { 'invalid-bounds-object': { messageRegex: /invalidBoundsObject/, friendlyMessage: 'Bounds object is not valid. Use a carto.filter.Bounds object' + }, + 'column-required': { + messageRegex: /columnRequired/, + friendlyMessage: 'Column property is required.' + }, + 'column-string': { + messageRegex: /columnString/, + friendlyMessage: 'Column property must be a string.' + }, + 'empty-column': { + messageRegex: /emptyColumn/, + friendlyMessage: 'Column property must be not empty.' + }, + 'invalid-filter': { + messageRegex: /invalidFilter(.+)/, + // TODO: Add link to documentation? + friendlyMessage: "'$0' is not a valid filter. Please check documentation." + }, + 'invalid-option': { + messageRegex: /invalidOption(.+)/, + friendlyMessage: "'$0' is not a valid option for this filter." } }, aggregation: { diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js new file mode 100644 index 0000000000..636e658a82 --- /dev/null +++ b/src/api/v4/filter/base-sql.js @@ -0,0 +1,117 @@ +const _ = require('underscore'); +const Base = require('./base'); + +const ALLOWED_OPTIONS = ['includeNull']; +const DEFAULT_JOIN_OPERATOR = 'AND'; + +/** + * SQL Filter + * + * A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter + * + * @class carto.filter.SQLBase + * @extends carto.filter.Base + * @memberof carto.filter + * @api + */ +class SQLBase extends Base { + /** + * Creates an instance of SQLBase. + * @param {string} column - The filtering will be performed against this column + * @param {object} [options={}] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.reverseConditions] - The operation to apply to the data + * @memberof carto.filter.SQLBase + */ + constructor (column, options = {}) { + super(); + + this._checkColumn(column); + this._checkOptions(options); + + this._column = column; + this._filters = {}; + this._options = options; + } + + set (filterType, filterValue) { + const newFilter = { [filterType]: filterValue }; + + this._checkFilters(newFilter); + this._filters[filterType] = filterValue; + + this.trigger('change:filters', newFilter); + } + + setFilters (filters) { + this._checkFilters(filters); + this._filters = filters; + + this.trigger('change:filters', filters); + } + + getSQL () { + return Object.keys(this._filters) + .map(filterType => this._interpolateFilterIntoTemplate(filterType, this._filters[filterType])) + .join(` ${DEFAULT_JOIN_OPERATOR} `); + } + + _checkColumn (column) { + if (_.isUndefined(column)) { + throw this._getValidationError('columnRequired'); + } + + if (!_.isString(column)) { + throw this._getValidationError('columnString'); + } + + if (_.isEmpty(column)) { + throw this._getValidationError('emptyColumn'); + } + } + + _checkFilters (filters) { + Object.keys(filters).forEach(filter => { + const isFilterValid = _.contains(this.ALLOWED_FILTERS, filter); + + if (!isFilterValid) { + throw this._getValidationError(`invalidFilter${filter}`); + } + }); + } + + _checkOptions (options) { + Object.keys(options).forEach(option => { + const isOptionValid = _.contains(ALLOWED_OPTIONS, option); + + if (!isOptionValid) { + throw this._getValidationError(`invalidOption${option}`); + } + }); + } + + _convertValueToSQLString (filterValue) { + if (_.isDate(filterValue)) { + return filterValue.toISOString(); + } + + if (_.isArray(filterValue)) { + return filterValue + .map(value => `'${value.toString()}'`) + .join(','); + } + + if (_.isObject(filterValue) || _.isNumber(filterValue)) { + return filterValue; + } + + return `'${filterValue.toString()}'`; + } + + _interpolateFilterIntoTemplate (filterType, filterValue) { + const sqlString = _.template(this.SQL_TEMPLATES[filterType]); + return sqlString({ column: this._column, value: this._convertValueToSQLString(filterValue) }); + } +} + +module.exports = SQLBase; diff --git a/src/api/v4/filter/sql/category.js b/src/api/v4/filter/category.js similarity index 67% rename from src/api/v4/filter/sql/category.js rename to src/api/v4/filter/category.js index 0560b638a3..1ffae574f1 100644 --- a/src/api/v4/filter/sql/category.js +++ b/src/api/v4/filter/category.js @@ -1,13 +1,13 @@ -const SQLBaseFilter = require('./sql-base-filter'); const _ = require('underscore'); +const SQLBase = require('./base-sql'); const CATEGORY_COMPARISON_OPERATORS = { - EQ: 'eq', - NOT_EQ: 'not_eq', IN: 'in', - NOT_IN: 'not_in', + NOT_IN: 'notIn', + EQ: 'eq', + NOT_EQ: 'notEq', LIKE: 'like', - SIMILAR_TO: 'similar_to' + SIMILAR_TO: 'similarTo' }; const ALLOWED_FILTERS = Object.freeze(_.values(CATEGORY_COMPARISON_OPERATORS)); @@ -18,37 +18,40 @@ const ALLOWED_FILTERS = Object.freeze(_.values(CATEGORY_COMPARISON_OPERATORS)); * When including this filter into a {@link source.sql} or a {@link source.dataset}, the rows will be filtered by the conditions included within the filter. * * @class carto.filter.Category - * @extends carto.filter.SQLBaseFilter + * @extends carto.filter.SQLBase * @memberof carto.filter * @api */ -class Category extends SQLBaseFilter { +class Category extends SQLBase { /** * Create a Category Filter * @param {string} column - The column which the filter will be performed against - * @param {object} [filters] - The filters that you want to apply to the table rows - * @param {(string|number|Date)} [filters.eq] - Filter rows whose column value is equal to the provided value - * @param {(string|number|Date)} [filters.neq] - Filter rows whose column value is not equal to the provided value + * @param {object} filters - The filters that you want to apply to the table rows * @param {string[]} [filters.in] - Filter rows whose column value is included within the provided values - * @param {string[]} [filters.not_in] - Filter rows whose column value is included within the provided values + * @param {string[]} [filters.notIn] - Filter rows whose column value is included within the provided values + * @param {(string|number|Date)} [filters.eq] - Filter rows whose column value is equal to the provided value + * @param {(string|number|Date)} [filters.notEq] - Filter rows whose column value is not equal to the provided value * @param {string} [filters.like] - Filter rows whose column value is like the provided value - * @param {string} [filters.similar_to] - Filter rows whose column value is similar to the provided values + * @param {string} [filters.similarTo] - Filter rows whose column value is similar to the provided values * @param {object} [options] * @param {boolean} [options.includeNull] - The operation to apply to the data * @param {boolean} [options.reverseConditions] - The operation to apply to the data */ - constructor (column, filters, options) { + constructor (column, filters = {}, options) { super(column, options); + this.SQL_TEMPLATES = this._getSQLTemplates(); this.ALLOWED_FILTERS = ALLOWED_FILTERS; this._checkFilters(filters); this._filters = filters; + } - this.SQL_TEMPLATES = { + _getSQLTemplates () { + return { [CATEGORY_COMPARISON_OPERATORS.IN]: '<%= column %> IN (<%= value %>)', [CATEGORY_COMPARISON_OPERATORS.NOT_IN]: '<%= column %> NOT IN (<%= value %>)', - [CATEGORY_COMPARISON_OPERATORS.EQ]: '<%= column %> == <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.EQ]: '<%= column %> = <%= value %>', [CATEGORY_COMPARISON_OPERATORS.NOT_EQ]: '<%= column %> != <%= value %>', [CATEGORY_COMPARISON_OPERATORS.LIKE]: '<%= column %> LIKE <%= value %>', [CATEGORY_COMPARISON_OPERATORS.SIMILAR_TO]: '<%= column %> SIMILAR TO <%= value %>' diff --git a/src/api/v4/filter/index.js b/src/api/v4/filter/index.js index 5ce8b44d7e..33cc029f10 100644 --- a/src/api/v4/filter/index.js +++ b/src/api/v4/filter/index.js @@ -1,8 +1,8 @@ const BoundingBox = require('./bounding-box'); const BoundingBoxLeaflet = require('./bounding-box-leaflet'); const BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); -const Category = require('./sql/category'); -const Range = require('./sql/range'); +const Category = require('./category'); +const Range = require('./range'); /** * @namespace carto.filter diff --git a/src/api/v4/filter/sql/range.js b/src/api/v4/filter/range.js similarity index 78% rename from src/api/v4/filter/sql/range.js rename to src/api/v4/filter/range.js index bffb5536a5..0d1e2e3c91 100644 --- a/src/api/v4/filter/sql/range.js +++ b/src/api/v4/filter/range.js @@ -1,5 +1,5 @@ -const SQLBaseFilter = require('./sql-base-filter'); const _ = require('underscore'); +const SQLBase = require('./base-sql'); const RANGE_COMPARISON_OPERATORS = { LT: 'lt', @@ -7,9 +7,9 @@ const RANGE_COMPARISON_OPERATORS = { GT: 'gt', GTE: 'gte', BETWEEN: 'between', - NOT_BETWEEN: 'not_between', - BETWEEN_SYMMETRIC: 'between_symmetric', - NOT_BETWEEN_SYMMETRIC: 'not_between_symmetric' + NOT_BETWEEN: 'notBetween', + BETWEEN_SYMMETRIC: 'betweenSymmetric', + NOT_BETWEEN_SYMMETRIC: 'notBetweenSymmetric' }; const ALLOWED_FILTERS = Object.freeze(_.values(RANGE_COMPARISON_OPERATORS)); @@ -24,10 +24,10 @@ const ALLOWED_FILTERS = Object.freeze(_.values(RANGE_COMPARISON_OPERATORS)); * @memberof carto.filter * @api */ -class Range extends SQLBaseFilter { +class Range extends SQLBase { /** * Create a Range filter - * + * //TODO: poner not between y not between symmetric * @param {string} column - The column to filter rows * @param {object} [filters] - The filters you want to apply to the column * @param {(number|Date)} [filters.lt] - Filter rows whose column value is less than the provided value @@ -37,22 +37,25 @@ class Range extends SQLBaseFilter { * @param {(number|Date)} [filters.between] - Filter rows whose column value is between the provided values * @param {(number|Date)} [filters.between.min] - Lowest value of the comparison range * @param {(number|Date)} [filters.between.max] - Upper value of the comparison range - * @param {(number|Date)} [filters.between_symmetric] - Filter rows whose column value is between the provided values after sorting them - * @param {(number|Date)} [filters.between_symmetric.min] - Lowest value of the comparison range - * @param {(number|Date)} [filters.between_symmetric.max] - Upper value of the comparison range + * @param {(number|Date)} [filters.betweenSymmetric] - Filter rows whose column value is between the provided values after sorting them + * @param {(number|Date)} [filters.betweenSymmetric.min] - Lowest value of the comparison range + * @param {(number|Date)} [filters.betweenSymmetric.max] - Upper value of the comparison range * @param {object} [options] * @param {boolean} [options.includeNull] - The operation to apply to the data * @param {boolean} [options.reverseConditions] - The operation to apply to the data */ - constructor (column, filters, options) { + constructor (column, filters = {}, options) { super(column, options); + this.SQL_TEMPLATES = this._getSQLTemplates(); this.ALLOWED_FILTERS = ALLOWED_FILTERS; this._checkFilters(filters); this._filters = filters; + } - this.SQL_TEMPLATES = { + _getSQLTemplates () { + return { [RANGE_COMPARISON_OPERATORS.LT]: '<%= column %> < <%= value %>', [RANGE_COMPARISON_OPERATORS.LTE]: '<%= column %> <= <%= value %>', [RANGE_COMPARISON_OPERATORS.GT]: '<%= column %> > <%= value %>', diff --git a/src/api/v4/filter/sql/sql-base-filter.js b/src/api/v4/filter/sql/sql-base-filter.js deleted file mode 100644 index 05f699b9bc..0000000000 --- a/src/api/v4/filter/sql/sql-base-filter.js +++ /dev/null @@ -1,123 +0,0 @@ -const Base = require('../base'); -const _ = require('underscore'); - -const ALLOWED_OPTIONS = ['includeNull']; -const DEFAULT_JOIN_OPERATOR = 'AND'; - -/** - * SQL Filter - * - * A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter - * - * @param {string} column - The filtering will be performed against this column - * @param {object} [filters] - The filters that you want to apply to the table rows - * @param {object} [options] - * @param {boolean} [options.includeNull] - The operation to apply to the data - * @param {boolean} [options.reverseConditions] - The operation to apply to the data - * - * - * @constructor - * @extends carto.filter.Base - * @memberof carto.filter - * @api - */ -class SQLFilterBase extends Base { - constructor (column, options = {}) { - super(); - - this._checkColumn(column); - this._checkOptions(options); - - this._column = column; - this._options = options; - } - - set (filterType, filterValue) { - const filter = { [filterType]: filterValue }; - - this._checkFilters(filter); - this._filters[filterType] = filterValue; - - this.trigger('change:filters', filter); - } - - setFilters (filters) { - this._checkFilters(filters); - _.extend(this._filters, filters); - - this.trigger('change:filters', filters); - } - - _checkColumn (column) { - if (_.isUndefined(column)) { - throw this._getValidationError('columnRequired'); - } - - if (!_.isString(column)) { - throw this._getValidationError('columnString'); - } - - if (_.isEmpty(column)) { - throw this._getValidationError('emptyColumn'); - } - } - - _checkFilters (filters) { - Object.keys(filters) - .forEach(filter => { - const isFilterAllowed = _.contains(this.ALLOWED_FILTERS, filter); - - if (!isFilterAllowed) { - console.error({filter}, 'is not allowed'); - // TODO: Return a better error - throw this._getValidationError('filterNotFound'); - } - }); - } - - _checkOptions (options) { - Object.keys(options).forEach(filter => { - const isOptionAllowed = _.contains(ALLOWED_OPTIONS, filter); - - if (!isOptionAllowed) { - // TODO: Return a better error - throw this._getValidationError('optionNotFound'); - } - }); - } - - _getFilterStringValue (filterValue) { - if (_.isDate(filterValue)) { - return filterValue.toISOString(); - } - - if (_.isArray(filterValue)) { - return filterValue - .map(value => `'${value.toString()}'`) - .join(','); - } - - if (_.isNumber(filterValue)) { - return filterValue.toString(); - } - - if (_.isObject(filterValue)) { - return filterValue; - } - - return `'${filterValue.toString()}'`; - } - - _getSQLString () { - return Object.keys(this._filters) - .map(filterType => this._buildFilterString(filterType, this._filters[filterType])) - .join(` ${DEFAULT_JOIN_OPERATOR} `); - } - - _buildFilterString (filterType, filterValue) { - const sqlStringTemplate = _.template(this.SQL_TEMPLATES[filterType]); - return sqlStringTemplate({ column: this._column, value: this._getFilterStringValue(filterValue) }); - } -} - -module.exports = SQLFilterBase; diff --git a/test/spec/api/v4/filter/base-sql.spec.js b/test/spec/api/v4/filter/base-sql.spec.js new file mode 100644 index 0000000000..115e33e627 --- /dev/null +++ b/test/spec/api/v4/filter/base-sql.spec.js @@ -0,0 +1,177 @@ +const SQLBase = require('../../../../../src/api/v4/filter/base-sql'); + +describe('api/v4/filter/sql/sql-filter-base', function () { + describe('constructor', function () { + it('should throw a descriptive error when column is undefined, not a string, or empty', function () { + expect(function () { + new SQLBase(undefined); // eslint-disable-line + }).toThrowError('Column property is required.'); + + expect(function () { + new SQLBase(1); // eslint-disable-line + }).toThrowError('Column property must be a string.'); + + expect(function () { + new SQLBase(''); // eslint-disable-line + }).toThrowError('Column property must be not empty.'); + }); + + it('should throw a descriptive error when there is an invalid option', function () { + expect(function () { + new SQLBase('fake_column', { unknown_option: false }); // eslint-disable-line + }).toThrowError("'unknown_option' is not a valid option for this filter."); + }); + + it('should set column and options as class properties', function () { + const column = 'fake_column'; + const options = { includeNull: true }; + + const sqlFilter = new SQLBase(column, options); + + expect(sqlFilter._column).toBe(column); + expect(sqlFilter._options).toBe(options); + }); + }); + + describe('.set', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + const sqlFilter = new SQLBase('fake_column'); + + expect(function () { + sqlFilter.set('unknown_filter', 'test_filter'); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it('should set the new filter to the filters object', function () { + const column = 'fake_column'; + + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in']; + + sqlFilter.set('in', ['test_filter']); + + expect(sqlFilter._filters).toEqual({ in: ['test_filter'] }); + }); + + it("should trigger a 'change:filters' event", function () { + const spy = jasmine.createSpy(); + + const sqlFilter = new SQLBase('fake_column'); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.on('change:filters', spy); + + sqlFilter.set('in', ['test_filter']); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('.setFilters', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + const sqlFilter = new SQLBase('fake_column'); + + expect(function () { + sqlFilter.setFilters({ unknown_filter: 'test_filter' }); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it('should set the new filters and override previous ones', function () { + const column = 'fake_column'; + const newFilters = { notIn: 'test_filter2' }; + + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.set('in', ['test_filter']); + + sqlFilter.setFilters(newFilters); + + expect(sqlFilter._filters).toEqual(newFilters); + }); + + it("should trigger a 'change:filters' event", function () { + const column = 'fake_column'; + const newFilters = { notIn: 'test_filter2' }; + const spy = jasmine.createSpy(); + + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.set('in', ['test_filter']); + sqlFilter.on('change:filters', spy); + + sqlFilter.setFilters(newFilters); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('.getSQL', function () { + it('should return SQL string containing all the filters joined by AND clause', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + sqlFilter.ALLOWED_FILTERS = ['in', 'like']; + sqlFilter.SQL_TEMPLATES = { + 'in': '<%= column %> IN (<%= value %>)', + 'like': '<%= column %> LIKE <%= value %>' + }; + sqlFilter.setFilters({ in: ['category 1', 'category 2'], like: '%category%' }); + + expect(sqlFilter.getSQL()).toBe("fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%'"); + }); + }); + + describe('._convertValueToSQLString', function () { + it('should format date to ISO8601 string', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + const fakeDate = new Date('Thu Jun 28 2018 15:04:31 GMT+0200 (Central European Summer Time)'); + expect(sqlFilter._convertValueToSQLString(fakeDate)).toBe('2018-06-28T13:04:31.000Z'); + }); + + it('should convert array to a comma-separated string wrapped by single comma', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + const fakeArray = ['Element 1', 'Element 2']; + expect(sqlFilter._convertValueToSQLString(fakeArray)).toBe("'Element 1','Element 2'"); + }); + + it('should return number without modifying', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + expect(sqlFilter._convertValueToSQLString(1)).toBe(1); + }); + + it('should return object without modifying', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + const fakeObject = { fakeProperty: 'fakeValue' }; + + expect(sqlFilter._convertValueToSQLString(fakeObject)).toBe(fakeObject); + }); + + it('should wrap strings in single-quotes', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + const fakeString = 'fake_string'; + + expect(sqlFilter._convertValueToSQLString(fakeString)).toBe(`'${fakeString}'`); + }); + + describe('._interpolateFilterIntoTemplate', function () { + it('should inject filter values into SQL template', function () { + const column = 'fake_column'; + const sqlFilter = new SQLBase(column); + + sqlFilter.SQL_TEMPLATES = { + 'GTE': '<%= column %> > <%= value %>' + }; + + expect(sqlFilter._interpolateFilterIntoTemplate('GTE', 10)).toBe('fake_column > 10'); + }); + }); + }); +}); diff --git a/test/spec/api/v4/filter/category.spec.js b/test/spec/api/v4/filter/category.spec.js new file mode 100644 index 0000000000..c145f382c5 --- /dev/null +++ b/test/spec/api/v4/filter/category.spec.js @@ -0,0 +1,43 @@ +const carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/sql/category', function () { + describe('constructor', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + expect(function () { + new carto.filter.Category('fake_column', { unknown_filter: '' }); // eslint-disable-line + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + }); + + describe('SQL Templates', function () { + it('IN', function () { + const categoryFilter = new carto.filter.Category('fake_column', { in: ['Category 1'] }); + expect(categoryFilter.getSQL()).toBe("fake_column IN ('Category 1')"); + }); + + it('NOT IN', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notIn: ['Category 1'] }); + expect(categoryFilter.getSQL()).toBe("fake_column NOT IN ('Category 1')"); + }); + + it('EQ', function () { + const categoryFilter = new carto.filter.Category('fake_column', { eq: 'Category 1' }); + expect(categoryFilter.getSQL()).toBe("fake_column = 'Category 1'"); + }); + + it('NOT EQ', function () { + const categoryFilter = new carto.filter.Category('fake_column', { notEq: 'Category 1' }); + expect(categoryFilter.getSQL()).toBe("fake_column != 'Category 1'"); + }); + + it('LIKE', function () { + const categoryFilter = new carto.filter.Category('fake_column', { like: '%Category%' }); + expect(categoryFilter.getSQL()).toBe("fake_column LIKE '%Category%'"); + }); + + it('SIMILAR TO', function () { + const categoryFilter = new carto.filter.Category('fake_column', { similarTo: '%Category%' }); + expect(categoryFilter.getSQL()).toBe("fake_column SIMILAR TO '%Category%'"); + }); + }); +}); diff --git a/test/spec/api/v4/filter/range.spec.js b/test/spec/api/v4/filter/range.spec.js new file mode 100644 index 0000000000..3e3d3dcb41 --- /dev/null +++ b/test/spec/api/v4/filter/range.spec.js @@ -0,0 +1,53 @@ +const carto = require('../../../../../src/api/v4/index'); + +describe('api/v4/filter/sql/range', function () { + describe('constructor', function () { + it('should throw a descriptive error when an unknown filter has been passed', function () { + expect(function () { + new carto.filter.Range('fake_column', { unknown_filter: '' }); // eslint-disable-line + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + }); + + describe('SQL Templates', function () { + it('LT', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lt: 10 }); + expect(categoryFilter.getSQL()).toBe('fake_column < 10'); + }); + + it('LTE', function () { + const categoryFilter = new carto.filter.Range('fake_column', { lte: 10 }); + expect(categoryFilter.getSQL()).toBe('fake_column <= 10'); + }); + + it('GT', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gt: 10 }); + expect(categoryFilter.getSQL()).toBe('fake_column > 10'); + }); + + it('GTE', function () { + const categoryFilter = new carto.filter.Range('fake_column', { gte: 10 }); + expect(categoryFilter.getSQL()).toBe('fake_column >= 10'); + }); + + it('BETWEEN', function () { + const categoryFilter = new carto.filter.Range('fake_column', { between: { min: 1, max: 10 } }); + expect(categoryFilter.getSQL()).toBe('fake_column BETWEEN 1 AND 10'); + }); + + it('NOT BETWEEN', function () { + const categoryFilter = new carto.filter.Range('fake_column', { notBetween: { min: 1, max: 10 } }); + expect(categoryFilter.getSQL()).toBe('fake_column NOT BETWEEN 1 AND 10'); + }); + + it('BETWEEN SYMMETRIC', function () { + const categoryFilter = new carto.filter.Range('fake_column', { betweenSymmetric: { min: 1, max: 10 } }); + expect(categoryFilter.getSQL()).toBe('fake_column BETWEEN SYMMETRIC 1 AND 10'); + }); + + it('NOT BETWEEN SYMMETRIC', function () { + const categoryFilter = new carto.filter.Range('fake_column', { notBetweenSymmetric: { min: 1, max: 10 } }); + expect(categoryFilter.getSQL()).toBe('fake_column NOT BETWEEN SYMMETRIC 1 AND 10'); + }); + }); +}); From 867f67f9be7c9ee9f82befd6108676dfb3224593 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Fri, 29 Jun 2018 12:00:13 +0200 Subject: [PATCH 06/35] Add OR, and AND filters. Integrate filters into SQL source. --- .../error-list/validation-errors.js | 4 ++ src/api/v4/filter/index.js | 5 +- src/api/v4/source/base.js | 37 +++++++--- src/api/v4/source/sql.js | 49 ++++++++++--- src/filters/filters-collection.js | 70 +++++++++++++++++++ 5 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 src/filters/filters-collection.js diff --git a/src/api/v4/error-handling/error-list/validation-errors.js b/src/api/v4/error-handling/error-list/validation-errors.js index a79dca2f20..882c16bae2 100644 --- a/src/api/v4/error-handling/error-list/validation-errors.js +++ b/src/api/v4/error-handling/error-list/validation-errors.js @@ -210,6 +210,10 @@ module.exports = { 'invalid-option': { messageRegex: /invalidOption(.+)/, friendlyMessage: "'$0' is not a valid option for this filter." + }, + 'wrong-filter-type': { + messageRegex: /wrongFilterType/, + friendlyMessage: 'Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.' } }, aggregation: { diff --git a/src/api/v4/filter/index.js b/src/api/v4/filter/index.js index 33cc029f10..d6635dede9 100644 --- a/src/api/v4/filter/index.js +++ b/src/api/v4/filter/index.js @@ -3,6 +3,7 @@ const BoundingBoxLeaflet = require('./bounding-box-leaflet'); const BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); const Category = require('./category'); const Range = require('./range'); +const { AND, OR } = require('../../../filters/filters-collection'); /** * @namespace carto.filter @@ -13,5 +14,7 @@ module.exports = { BoundingBoxLeaflet, BoundingBoxGoogleMaps, Category, - Range + Range, + AND, + OR }; diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index ca61c51337..328fbd10c5 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -1,15 +1,16 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); -var CartoError = require('../error-handling/carto-error'); -var EVENTS = require('../events'); +const _ = require('underscore'); +const Backbone = require('backbone'); +const CartoError = require('../error-handling/carto-error'); +const { FiltersCollection } = require('../../../filters/filters-collection'); +const EVENTS = require('../events'); /** * Base data source object. - * + * * The methods listed in the {@link carto.source.Base|source.Base} object are available in all source objects. - * + * * Use a source to reference the data used in a {@link carto.dataview.Base|dataview} or a {@link carto.layer.Base|layer}. - * + * * {@link carto.source.Base} should not be used directly use {@link carto.source.Dataset} or {@link carto.source.SQL} instead. * * @constructor @@ -20,6 +21,8 @@ var EVENTS = require('../events'); */ function Base () { this._id = Base.$generateId(); + this._hasFiltersApplied = false; + this._appliedFilters = new FiltersCollection(); } _.extend(Base.prototype, Backbone.Events); @@ -38,7 +41,7 @@ Base.$generateId = function () { /** * Return a unique autogenerated id. - * + * * @return {string} Unique autogenerated id */ Base.prototype.getId = function () { @@ -79,4 +82,22 @@ Base.prototype.$getInternalModel = function () { return this._internalModel; }; +Base.prototype.addFilter = function (filter) { + this._appliedFilters.add(filter); + this._hasFiltersApplied = true; +}; + +Base.prototype.addFilters = function (filters) { + filters.forEach(filter => this.addFilter(filter)); +}; + +Base.prototype.removeFilter = function (filter) { + this._appliedFilters.remove(filter); + this._hasFiltersApplied = Boolean(this._appliedFilters.count()); +}; + +Base.prototype.removeFilters = function (filters) { + filters.forEach(filter => this.removeFilter(filter)); +}; + module.exports = Base; diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js index 1a8b4c018d..bdada9b4c1 100644 --- a/src/api/v4/source/sql.js +++ b/src/api/v4/source/sql.js @@ -19,9 +19,12 @@ var CartoError = require('../error-handling/carto-error'); * @api */ function SQL (query) { + Base.apply(this, arguments); + _checkQuery(query); this._query = query; - Base.apply(this, arguments); + + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); } SQL.prototype = Object.create(Base.prototype); @@ -29,7 +32,7 @@ SQL.prototype = Object.create(Base.prototype); /** * Update the query. This method is asyncronous and returns a promise which is resolved when the style * is changed succesfully. It also fires a 'queryChanged' event. - * + * * @param {string} query - The sql query that will be the source of the data * @fires queryChanged * @returns {Promise} - A promise that will be fulfilled when the reload cycle is completed @@ -38,19 +41,15 @@ SQL.prototype = Object.create(Base.prototype); SQL.prototype.setQuery = function (query) { _checkQuery(query); this._query = query; + + const sqlString = this._getQueryToApply(); + if (!this._internalModel) { - this._triggerQueryChanged(this, query); + this._triggerQueryChanged(this, sqlString); return Promise.resolve(); } - this._internalModel.set('query', query, { silent: true }); - return this._internalModel._engine.reload() - .then(function () { - this._triggerQueryChanged(this, query); - }.bind(this)) - .catch(function (windshaftError) { - return Promise.reject(new CartoError(windshaftError)); - }); + return this._updateInternalModelQuery(sqlString); }; /** @@ -83,6 +82,34 @@ SQL.prototype._createInternalModel = function (engine) { return internalModel; }; +SQL.prototype._updateInternalModelQuery = function (query) { + this._internalModel.set('query', query, { silent: true }); + + return this._internalModel._engine.reload() + .then(() => this._triggerQueryChanged(this, query)) + .catch(windshaftError => Promise.reject(new CartoError(windshaftError))); +}; + +SQL.prototype._getQueryToApply = function () { + const whereClause = this._appliedFilters.getSQL(); + + if (!this._hasFiltersApplied || _.isEmpty(whereClause)) { + return this._query; + } + + return `SELECT * FROM (${this._query}) as originalQuery WHERE ${whereClause}`; +}; + +SQL.prototype.addFilter = function (filter) { + Base.prototype.addFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +SQL.prototype.removeFilter = function (filters) { + Base.prototype.removeFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + SQL.prototype._triggerQueryChanged = function (model, value) { this.trigger('queryChanged', value); }; diff --git a/src/filters/filters-collection.js b/src/filters/filters-collection.js new file mode 100644 index 0000000000..e041a38278 --- /dev/null +++ b/src/filters/filters-collection.js @@ -0,0 +1,70 @@ +const _ = require('underscore'); +const Base = require('../api/v4/filter/base'); +const SQLBase = require('../api/v4/filter/base-sql'); + +const DEFAULT_JOIN_OPERATOR = 'AND'; + +class FiltersCollection extends Base { + constructor (filters) { + super(); + this._initialize(filters); + } + + _initialize (filters) { + this._filters = []; + + if (filters && filters.length) { + filters.map(filter => this.add(filter)); + } + } + + add (filter) { + if (!(filter instanceof SQLBase)) { + throw this._getValidationError('wrongFilterType'); + } + + if (_.contains(this._filters, filter)) return; + + filter.on('change:filters', filters => this._triggerFilterChange(filters)); + this._filters.push(filter); + } + + remove (filter) { + if (!_.contains(this._filters, filter)) return; + + return this._filters.splice(_.indexOf(filter), 1); + } + + count () { + return this._filters.length; + } + + getSQL () { + return this._filters.map(filter => filter.getSQL()) + .join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `); + } + + _triggerFilterChange (filters) { + this.trigger('change:filters', filters); + } +} + +class AND extends FiltersCollection { + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'AND'; + } +} + +class OR extends FiltersCollection { + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'OR'; + } +} + +module.exports = { + FiltersCollection, + AND, + OR +}; From 56d462daeef750957babbb8fe53a44ac5a0fe689 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Fri, 29 Jun 2018 15:53:11 +0200 Subject: [PATCH 07/35] Wrap Date string in single comma. Integrate filters into dataset source. Parameter type checking. --- src/api/v4/filter/base-sql.js | 14 ++++++++++++- src/api/v4/filter/category.js | 29 +++++++++++++++------------ src/api/v4/filter/range.js | 37 +++++++++++++++++++---------------- src/api/v4/source/dataset.js | 36 ++++++++++++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 636e658a82..e62f6d229d 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -77,6 +77,14 @@ class SQLBase extends Base { if (!isFilterValid) { throw this._getValidationError(`invalidFilter${filter}`); } + + const hasCorrectType = this.PARAMETER_SPECIFICATION[filter].allowedTypes.some( + parameterType => parameterIsOfType(parameterType, filters[filter]) + ); + + if (!hasCorrectType) { + throw this._getValidationError(`invalidFilterParameterType${filter}`); + } }); } @@ -92,7 +100,7 @@ class SQLBase extends Base { _convertValueToSQLString (filterValue) { if (_.isDate(filterValue)) { - return filterValue.toISOString(); + return `'${filterValue.toISOString()}'`; } if (_.isArray(filterValue)) { @@ -114,4 +122,8 @@ class SQLBase extends Base { } } +const parameterIsOfType = function (parameterType, parameterValue) { + return _[`is${parameterType}`](parameterValue); +}; + module.exports = SQLBase; diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 1ffae574f1..34a0d1ef2b 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -2,15 +2,17 @@ const _ = require('underscore'); const SQLBase = require('./base-sql'); const CATEGORY_COMPARISON_OPERATORS = { - IN: 'in', - NOT_IN: 'notIn', - EQ: 'eq', - NOT_EQ: 'notEq', - LIKE: 'like', - SIMILAR_TO: 'similarTo' + in: { parameterName: 'in', allowedTypes: ['Array', 'String'] }, + not_in: { parameterName: 'notIn', allowedTypes: ['Array', 'String'] }, + eq: { parameterName: 'eq', allowedTypes: ['String', 'Number', 'Date'] }, + not_eq: { parameterName: 'notEq', allowedTypes: ['String', 'Number', 'Date'] }, + like: { parameterName: 'like', allowedTypes: ['String'] }, + similar_to: { parameterName: 'like', allowedTypes: ['String'] } }; -const ALLOWED_FILTERS = Object.freeze(_.values(CATEGORY_COMPARISON_OPERATORS)); +const ALLOWED_FILTERS = Object.freeze( + _.values(CATEGORY_COMPARISON_OPERATORS).map(operator => operator.parameterName) +); /** * Category Filter @@ -42,6 +44,7 @@ class Category extends SQLBase { this.SQL_TEMPLATES = this._getSQLTemplates(); this.ALLOWED_FILTERS = ALLOWED_FILTERS; + this.PARAMETER_SPECIFICATION = CATEGORY_COMPARISON_OPERATORS; this._checkFilters(filters); this._filters = filters; @@ -49,12 +52,12 @@ class Category extends SQLBase { _getSQLTemplates () { return { - [CATEGORY_COMPARISON_OPERATORS.IN]: '<%= column %> IN (<%= value %>)', - [CATEGORY_COMPARISON_OPERATORS.NOT_IN]: '<%= column %> NOT IN (<%= value %>)', - [CATEGORY_COMPARISON_OPERATORS.EQ]: '<%= column %> = <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.NOT_EQ]: '<%= column %> != <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.LIKE]: '<%= column %> LIKE <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.SIMILAR_TO]: '<%= column %> SIMILAR TO <%= value %>' + [CATEGORY_COMPARISON_OPERATORS.in]: '<%= column %> IN (<%= value %>)', + [CATEGORY_COMPARISON_OPERATORS.not_in]: '<%= column %> NOT IN (<%= value %>)', + [CATEGORY_COMPARISON_OPERATORS.eq]: '<%= column %> = <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.not_eq]: '<%= column %> != <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.like]: '<%= column %> LIKE <%= value %>', + [CATEGORY_COMPARISON_OPERATORS.similar_to]: '<%= column %> SIMILAR TO <%= value %>' }; } } diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index 0d1e2e3c91..b9b27cba86 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -2,17 +2,19 @@ const _ = require('underscore'); const SQLBase = require('./base-sql'); const RANGE_COMPARISON_OPERATORS = { - LT: 'lt', - LTE: 'lte', - GT: 'gt', - GTE: 'gte', - BETWEEN: 'between', - NOT_BETWEEN: 'notBetween', - BETWEEN_SYMMETRIC: 'betweenSymmetric', - NOT_BETWEEN_SYMMETRIC: 'notBetweenSymmetric' + lt: { parameterName: 'lt', allowedTypes: ['Number', 'Date'] }, + lte: { parameterName: 'lte', allowedTypes: ['Number', 'Date'] }, + gt: { parameterName: 'gt', allowedTypes: ['Number', 'Date'] }, + gte: { parameterName: 'gte', allowedTypes: ['Number', 'Date'] }, + between: { parameterName: 'between', allowedTypes: ['Number', 'Date'] }, + not_between: { parameterName: 'not_between', allowedTypes: ['Number', 'Date'] }, + between_symmetric: { parameterName: 'between_symmetric', allowedTypes: ['Number', 'Date'] }, + not_between_symmetric: { parameterName: 'not_between_symmetric', allowedTypes: ['Number', 'Date'] } }; -const ALLOWED_FILTERS = Object.freeze(_.values(RANGE_COMPARISON_OPERATORS)); +const ALLOWED_FILTERS = Object.freeze( + _.values(RANGE_COMPARISON_OPERATORS).map(operator => operator.parameterName) +); /** * Range Filter @@ -49,6 +51,7 @@ class Range extends SQLBase { this.SQL_TEMPLATES = this._getSQLTemplates(); this.ALLOWED_FILTERS = ALLOWED_FILTERS; + this.PARAMETER_SPECIFICATION = RANGE_COMPARISON_OPERATORS; this._checkFilters(filters); this._filters = filters; @@ -56,14 +59,14 @@ class Range extends SQLBase { _getSQLTemplates () { return { - [RANGE_COMPARISON_OPERATORS.LT]: '<%= column %> < <%= value %>', - [RANGE_COMPARISON_OPERATORS.LTE]: '<%= column %> <= <%= value %>', - [RANGE_COMPARISON_OPERATORS.GT]: '<%= column %> > <%= value %>', - [RANGE_COMPARISON_OPERATORS.GTE]: '<%= column %> >= <%= value %>', - [RANGE_COMPARISON_OPERATORS.BETWEEN]: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.NOT_BETWEEN]: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.BETWEEN_SYMMETRIC]: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.NOT_BETWEEN_SYMMETRIC]: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' + [RANGE_COMPARISON_OPERATORS.lt]: '<%= column %> < <%= value %>', + [RANGE_COMPARISON_OPERATORS.lte]: '<%= column %> <= <%= value %>', + [RANGE_COMPARISON_OPERATORS.gt]: '<%= column %> > <%= value %>', + [RANGE_COMPARISON_OPERATORS.gte]: '<%= column %> >= <%= value %>', + [RANGE_COMPARISON_OPERATORS.between]: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.not_between]: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.between_symmetric]: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', + [RANGE_COMPARISON_OPERATORS.not_between_symmetric]: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' }; } } diff --git a/src/api/v4/source/dataset.js b/src/api/v4/source/dataset.js index ff91367f79..f71d9ad6bc 100644 --- a/src/api/v4/source/dataset.js +++ b/src/api/v4/source/dataset.js @@ -3,6 +3,7 @@ var Base = require('./base'); var AnalysisModel = require('../../../analysis/analysis-model'); var CamshaftReference = require('../../../analysis/camshaft-reference'); var CartoValidationError = require('../error-handling/carto-validation-error'); +var CartoError = require('../error-handling/carto-error'); /** * A Dataset that can be used as the data source for layers and dataviews. @@ -17,9 +18,12 @@ var CartoValidationError = require('../error-handling/carto-validation-error'); * @api */ function Dataset (tableName) { + Base.apply(this, arguments); + _checkTableName(tableName); this._tableName = tableName; - Base.apply(this, arguments); + + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); } Dataset.prototype = Object.create(Base.prototype); @@ -43,7 +47,7 @@ Dataset.prototype._createInternalModel = function (engine) { var internalModel = new AnalysisModel({ id: this.getId(), type: 'source', - query: 'SELECT * from ' + this._tableName + query: `SELECT * from ${this._tableName}` }, { camshaftReference: CamshaftReference, engine: engine @@ -52,6 +56,34 @@ Dataset.prototype._createInternalModel = function (engine) { return internalModel; }; +Dataset.prototype._updateInternalModelQuery = function (query) { + this._internalModel.set('query', query, { silent: true }); + + return this._internalModel._engine.reload() + .catch(windshaftError => Promise.reject(new CartoError(windshaftError))); +}; + +Dataset.prototype._getQueryToApply = function () { + const whereClause = this._appliedFilters.getSQL(); + const datasetQuery = `SELECT * from ${this._tableName}`; + + if (_.isEmpty(whereClause)) { + return datasetQuery; + } + + return `SELECT * FROM (${datasetQuery}) as datasetQuery WHERE ${whereClause}`; +}; + +Dataset.prototype.addFilter = function (filter) { + Base.prototype.addFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + +Dataset.prototype.removeFilter = function (filters) { + Base.prototype.removeFilter.apply(this, arguments); + this._updateInternalModelQuery(this._getQueryToApply()); +}; + function _checkTableName (tableName) { if (_.isUndefined(tableName)) { throw new CartoValidationError('source', 'noDatasetName'); From a83a58f54688e618f86d7490f07e187969f50f4c Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 08:34:21 +0200 Subject: [PATCH 08/35] AND and OR filter groups. Some fixes, and add tests. --- .../error-list/validation-errors.js | 4 + src/api/v4/filter/and.js | 20 +++ src/api/v4/filter/base-sql.js | 34 +++- src/api/v4/filter/category.js | 29 ++-- .../v4/filter}/filters-collection.js | 37 ++--- src/api/v4/filter/index.js | 3 +- src/api/v4/filter/or.js | 20 +++ src/api/v4/filter/range.js | 57 ++++--- src/api/v4/source/base.js | 6 +- test/spec/api/v4/filter/base-sql.spec.js | 100 +++++++++--- test/spec/api/v4/filter/category.spec.js | 2 +- .../api/v4/filter/filters-collection.spec.js | 150 ++++++++++++++++++ test/spec/api/v4/filter/range.spec.js | 2 +- test/spec/api/v4/source/dataset.spec.js | 61 ++++++- test/spec/api/v4/source/sql.spec.js | 61 ++++++- 15 files changed, 485 insertions(+), 101 deletions(-) create mode 100644 src/api/v4/filter/and.js rename src/{filters => api/v4/filter}/filters-collection.js (59%) create mode 100644 src/api/v4/filter/or.js create mode 100644 test/spec/api/v4/filter/filters-collection.spec.js diff --git a/src/api/v4/error-handling/error-list/validation-errors.js b/src/api/v4/error-handling/error-list/validation-errors.js index 882c16bae2..33403cfa25 100644 --- a/src/api/v4/error-handling/error-list/validation-errors.js +++ b/src/api/v4/error-handling/error-list/validation-errors.js @@ -214,6 +214,10 @@ module.exports = { 'wrong-filter-type': { messageRegex: /wrongFilterType/, friendlyMessage: 'Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.' + }, + 'invalid-parameter-type': { + messageRegex: /invalidParameterType(.+)/, + friendlyMessage: "Invalid parameter type for '$0'. Please check filters documentation." } }, aggregation: { diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js new file mode 100644 index 0000000000..e4c3574fe8 --- /dev/null +++ b/src/api/v4/filter/and.js @@ -0,0 +1,20 @@ +const FiltersCollection = require('./filters-collection'); + +class AND extends FiltersCollection { + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'AND'; + } + + getSQL () { + const sql = FiltersCollection.prototype.getSQL.apply(this); + + if (this.count() > 1) { + return `(${sql})`; + } + + return sql; + } +} + +module.exports = AND; diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index e62f6d229d..a6553bc94d 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -51,9 +51,15 @@ class SQLBase extends Base { } getSQL () { - return Object.keys(this._filters) - .map(filterType => this._interpolateFilterIntoTemplate(filterType, this._filters[filterType])) + const filters = Object.keys(this._filters); + let sql = filters.map(filterType => this._interpolateFilter(filterType, this._filters[filterType])) .join(` ${DEFAULT_JOIN_OPERATOR} `); + + if (this._options.includeNull) { + this._includeNullInQuery(sql); + } + + return sql; } _checkColumn (column) { @@ -78,12 +84,16 @@ class SQLBase extends Base { throw this._getValidationError(`invalidFilter${filter}`); } - const hasCorrectType = this.PARAMETER_SPECIFICATION[filter].allowedTypes.some( - parameterType => parameterIsOfType(parameterType, filters[filter]) + const parameters = this.PARAMETER_SPECIFICATION[filter].parameters; + const haveCorrectType = parameters.every( + parameter => { + const parameterValue = _.property(parameter.name)(filters[filter]) || filters[parameter.name]; + return parameter.allowedTypes.some(type => parameterIsOfType(type, parameterValue)); + } ); - if (!hasCorrectType) { - throw this._getValidationError(`invalidFilterParameterType${filter}`); + if (!haveCorrectType) { + throw this._getValidationError(`invalidParameterType${filter}`); } }); } @@ -116,10 +126,20 @@ class SQLBase extends Base { return `'${filterValue.toString()}'`; } - _interpolateFilterIntoTemplate (filterType, filterValue) { + _interpolateFilter (filterType, filterValue) { const sqlString = _.template(this.SQL_TEMPLATES[filterType]); return sqlString({ column: this._column, value: this._convertValueToSQLString(filterValue) }); } + + _includeNullInQuery (sql) { + const filters = Object.keys(this._filters); + + if (filters.length > 1) { + sql = `(${sql})`; + } + + return `(${sql} OR ${this._column} IS NULL)`; + } } const parameterIsOfType = function (parameterType, parameterValue) { diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 34a0d1ef2b..4d02956d06 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -1,18 +1,15 @@ -const _ = require('underscore'); const SQLBase = require('./base-sql'); const CATEGORY_COMPARISON_OPERATORS = { - in: { parameterName: 'in', allowedTypes: ['Array', 'String'] }, - not_in: { parameterName: 'notIn', allowedTypes: ['Array', 'String'] }, - eq: { parameterName: 'eq', allowedTypes: ['String', 'Number', 'Date'] }, - not_eq: { parameterName: 'notEq', allowedTypes: ['String', 'Number', 'Date'] }, - like: { parameterName: 'like', allowedTypes: ['String'] }, - similar_to: { parameterName: 'like', allowedTypes: ['String'] } + in: { parameters: [{ name: 'in', allowedTypes: ['Array', 'String'] }] }, + notIn: { parameters: [{ name: 'notIn', allowedTypes: ['Array', 'String'] }] }, + eq: { parameters: [{ name: 'eq', allowedTypes: ['String', 'Number', 'Date'] }] }, + notEq: { parameters: [{ name: 'notEq', allowedTypes: ['String', 'Number', 'Date'] }] }, + like: { parameters: [{ name: 'like', allowedTypes: ['String'] }] }, + similarTo: { parameters: [{ name: 'similarTo', allowedTypes: ['String'] }] } }; -const ALLOWED_FILTERS = Object.freeze( - _.values(CATEGORY_COMPARISON_OPERATORS).map(operator => operator.parameterName) -); +const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS)); /** * Category Filter @@ -52,12 +49,12 @@ class Category extends SQLBase { _getSQLTemplates () { return { - [CATEGORY_COMPARISON_OPERATORS.in]: '<%= column %> IN (<%= value %>)', - [CATEGORY_COMPARISON_OPERATORS.not_in]: '<%= column %> NOT IN (<%= value %>)', - [CATEGORY_COMPARISON_OPERATORS.eq]: '<%= column %> = <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.not_eq]: '<%= column %> != <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.like]: '<%= column %> LIKE <%= value %>', - [CATEGORY_COMPARISON_OPERATORS.similar_to]: '<%= column %> SIMILAR TO <%= value %>' + in: '<%= column %> IN (<%= value %>)', + notIn: '<%= column %> NOT IN (<%= value %>)', + eq: '<%= column %> = <%= value %>', + notEq: '<%= column %> != <%= value %>', + like: '<%= column %> LIKE <%= value %>', + similarTo: '<%= column %> SIMILAR TO <%= value %>' }; } } diff --git a/src/filters/filters-collection.js b/src/api/v4/filter/filters-collection.js similarity index 59% rename from src/filters/filters-collection.js rename to src/api/v4/filter/filters-collection.js index e041a38278..7bb52c6995 100644 --- a/src/filters/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -1,6 +1,6 @@ const _ = require('underscore'); -const Base = require('../api/v4/filter/base'); -const SQLBase = require('../api/v4/filter/base-sql'); +const Base = require('./base'); +const SQLBase = require('./base-sql'); const DEFAULT_JOIN_OPERATOR = 'AND'; @@ -14,12 +14,12 @@ class FiltersCollection extends Base { this._filters = []; if (filters && filters.length) { - filters.map(filter => this.add(filter)); + filters.map(filter => this.addFilter(filter)); } } - add (filter) { - if (!(filter instanceof SQLBase)) { + addFilter (filter) { + if (!(filter instanceof SQLBase) && !(filter instanceof FiltersCollection)) { throw this._getValidationError('wrongFilterType'); } @@ -27,12 +27,15 @@ class FiltersCollection extends Base { filter.on('change:filters', filters => this._triggerFilterChange(filters)); this._filters.push(filter); + this._triggerFilterChange(); } - remove (filter) { + removeFilter (filter) { if (!_.contains(this._filters, filter)) return; - return this._filters.splice(_.indexOf(filter), 1); + const removedElement = this._filters.splice(_.indexOf(filter), 1); + this._triggerFilterChange(); + return removedElement; } count () { @@ -49,22 +52,4 @@ class FiltersCollection extends Base { } } -class AND extends FiltersCollection { - constructor (filters) { - super(filters); - this.JOIN_OPERATOR = 'AND'; - } -} - -class OR extends FiltersCollection { - constructor (filters) { - super(filters); - this.JOIN_OPERATOR = 'OR'; - } -} - -module.exports = { - FiltersCollection, - AND, - OR -}; +module.exports = FiltersCollection; diff --git a/src/api/v4/filter/index.js b/src/api/v4/filter/index.js index d6635dede9..5715150588 100644 --- a/src/api/v4/filter/index.js +++ b/src/api/v4/filter/index.js @@ -3,7 +3,8 @@ const BoundingBoxLeaflet = require('./bounding-box-leaflet'); const BoundingBoxGoogleMaps = require('./bounding-box-gmaps'); const Category = require('./category'); const Range = require('./range'); -const { AND, OR } = require('../../../filters/filters-collection'); +const AND = require('./and'); +const OR = require('./or'); /** * @namespace carto.filter diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js new file mode 100644 index 0000000000..abb678b423 --- /dev/null +++ b/src/api/v4/filter/or.js @@ -0,0 +1,20 @@ +const FiltersCollection = require('./filters-collection'); + +class OR extends FiltersCollection { + constructor (filters) { + super(filters); + this.JOIN_OPERATOR = 'OR'; + } + + getSQL () { + const sql = FiltersCollection.prototype.getSQL.apply(this); + + if (this.count() > 1) { + return `(${sql})`; + } + + return sql; + } +} + +module.exports = OR; diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index b9b27cba86..6f9741dbae 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -1,20 +1,37 @@ -const _ = require('underscore'); const SQLBase = require('./base-sql'); const RANGE_COMPARISON_OPERATORS = { - lt: { parameterName: 'lt', allowedTypes: ['Number', 'Date'] }, - lte: { parameterName: 'lte', allowedTypes: ['Number', 'Date'] }, - gt: { parameterName: 'gt', allowedTypes: ['Number', 'Date'] }, - gte: { parameterName: 'gte', allowedTypes: ['Number', 'Date'] }, - between: { parameterName: 'between', allowedTypes: ['Number', 'Date'] }, - not_between: { parameterName: 'not_between', allowedTypes: ['Number', 'Date'] }, - between_symmetric: { parameterName: 'between_symmetric', allowedTypes: ['Number', 'Date'] }, - not_between_symmetric: { parameterName: 'not_between_symmetric', allowedTypes: ['Number', 'Date'] } + lt: { parameters: [{ name: 'lt', allowedTypes: ['Number', 'Date'] }] }, + lte: { parameters: [{ name: 'lte', allowedTypes: ['Number', 'Date'] }] }, + gt: { parameters: [{ name: 'gt', allowedTypes: ['Number', 'Date'] }] }, + gte: { parameters: [{ name: 'gte', allowedTypes: ['Number', 'Date'] }] }, + between: { + parameters: [ + { name: 'min', allowedTypes: ['Number', 'Date'] }, + { name: 'max', allowedTypes: ['Number', 'Date'] } + ] + }, + notBetween: { + parameters: [ + { name: 'min', allowedTypes: ['Number', 'Date'] }, + { name: 'max', allowedTypes: ['Number', 'Date'] } + ] + }, + betweenSymmetric: { + parameters: [ + { name: 'min', allowedTypes: ['Number', 'Date'] }, + { name: 'max', allowedTypes: ['Number', 'Date'] } + ] + }, + notBetweenSymmetric: { + parameters: [ + { name: 'min', allowedTypes: ['Number', 'Date'] }, + { name: 'max', allowedTypes: ['Number', 'Date'] } + ] + } }; -const ALLOWED_FILTERS = Object.freeze( - _.values(RANGE_COMPARISON_OPERATORS).map(operator => operator.parameterName) -); +const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); /** * Range Filter @@ -59,14 +76,14 @@ class Range extends SQLBase { _getSQLTemplates () { return { - [RANGE_COMPARISON_OPERATORS.lt]: '<%= column %> < <%= value %>', - [RANGE_COMPARISON_OPERATORS.lte]: '<%= column %> <= <%= value %>', - [RANGE_COMPARISON_OPERATORS.gt]: '<%= column %> > <%= value %>', - [RANGE_COMPARISON_OPERATORS.gte]: '<%= column %> >= <%= value %>', - [RANGE_COMPARISON_OPERATORS.between]: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.not_between]: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.between_symmetric]: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', - [RANGE_COMPARISON_OPERATORS.not_between_symmetric]: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' + lt: '<%= column %> < <%= value %>', + lte: '<%= column %> <= <%= value %>', + gt: '<%= column %> > <%= value %>', + gte: '<%= column %> >= <%= value %>', + between: '<%= column %> BETWEEN <%= value.min %> AND <%= value.max %>', + notBetween: '<%= column %> NOT BETWEEN <%= value.min %> AND <%= value.max %>', + betweenSymmetric: '<%= column %> BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>', + notBetweenSymmetric: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' }; } } diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index 328fbd10c5..e69433ea1c 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -1,7 +1,7 @@ const _ = require('underscore'); const Backbone = require('backbone'); const CartoError = require('../error-handling/carto-error'); -const { FiltersCollection } = require('../../../filters/filters-collection'); +const FiltersCollection = require('../filter/filters-collection'); const EVENTS = require('../events'); /** @@ -83,7 +83,7 @@ Base.prototype.$getInternalModel = function () { }; Base.prototype.addFilter = function (filter) { - this._appliedFilters.add(filter); + this._appliedFilters.addFilter(filter); this._hasFiltersApplied = true; }; @@ -92,7 +92,7 @@ Base.prototype.addFilters = function (filters) { }; Base.prototype.removeFilter = function (filter) { - this._appliedFilters.remove(filter); + this._appliedFilters.removeFilter(filter); this._hasFiltersApplied = Boolean(this._appliedFilters.count()); }; diff --git a/test/spec/api/v4/filter/base-sql.spec.js b/test/spec/api/v4/filter/base-sql.spec.js index 115e33e627..3829ff0a60 100644 --- a/test/spec/api/v4/filter/base-sql.spec.js +++ b/test/spec/api/v4/filter/base-sql.spec.js @@ -1,6 +1,19 @@ const SQLBase = require('../../../../../src/api/v4/filter/base-sql'); -describe('api/v4/filter/sql/sql-filter-base', function () { +const PARAMETER_SPECIFICATION = { + in: { parameters: [{ name: 'in', allowedTypes: ['Array', 'String'] }] }, + notIn: { parameters: [{ name: 'notIn', allowedTypes: ['Array', 'String'] }] }, + like: { parameters: [{ name: 'like', allowedTypes: ['Array', 'String'] }] } +}; + +const SQL_TEMPLATES = { + 'in': '<%= column %> IN (<%= value %>)', + 'like': '<%= column %> LIKE <%= value %>' +}; + +const column = 'fake_column'; + +describe('api/v4/filter/base-sql', function () { describe('constructor', function () { it('should throw a descriptive error when column is undefined, not a string, or empty', function () { expect(function () { @@ -23,7 +36,6 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it('should set column and options as class properties', function () { - const column = 'fake_column'; const options = { includeNull: true }; const sqlFilter = new SQLBase(column, options); @@ -43,10 +55,9 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it('should set the new filter to the filters object', function () { - const column = 'fake_column'; - const sqlFilter = new SQLBase(column); sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; sqlFilter.set('in', ['test_filter']); @@ -58,6 +69,7 @@ describe('api/v4/filter/sql/sql-filter-base', function () { const sqlFilter = new SQLBase('fake_column'); sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; sqlFilter.on('change:filters', spy); sqlFilter.set('in', ['test_filter']); @@ -76,11 +88,14 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it('should set the new filters and override previous ones', function () { - const column = 'fake_column'; const newFilters = { notIn: 'test_filter2' }; const sqlFilter = new SQLBase(column); sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + notIn: PARAMETER_SPECIFICATION.notIn + }; sqlFilter.set('in', ['test_filter']); sqlFilter.setFilters(newFilters); @@ -89,12 +104,15 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it("should trigger a 'change:filters' event", function () { - const column = 'fake_column'; const newFilters = { notIn: 'test_filter2' }; const spy = jasmine.createSpy(); const sqlFilter = new SQLBase(column); sqlFilter.ALLOWED_FILTERS = ['in', 'notIn']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + notIn: PARAMETER_SPECIFICATION.notIn + }; sqlFilter.set('in', ['test_filter']); sqlFilter.on('change:filters', spy); @@ -106,30 +124,68 @@ describe('api/v4/filter/sql/sql-filter-base', function () { describe('.getSQL', function () { it('should return SQL string containing all the filters joined by AND clause', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); sqlFilter.ALLOWED_FILTERS = ['in', 'like']; + sqlFilter.PARAMETER_SPECIFICATION = { + in: PARAMETER_SPECIFICATION.in, + like: PARAMETER_SPECIFICATION.like + }; sqlFilter.SQL_TEMPLATES = { - 'in': '<%= column %> IN (<%= value %>)', - 'like': '<%= column %> LIKE <%= value %>' + in: SQL_TEMPLATES.in, + like: SQL_TEMPLATES.like }; sqlFilter.setFilters({ in: ['category 1', 'category 2'], like: '%category%' }); expect(sqlFilter.getSQL()).toBe("fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%'"); }); + + it('should call _includeNullInQuery if includeNull option is set', function () { + const sqlFilter = new SQLBase(column, { includeNull: true }); + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + sqlFilter.SQL_TEMPLATES = { in: SQL_TEMPLATES.in }; + sqlFilter.setFilters({ in: ['category 1', 'category 2'] }); + + spyOn(sqlFilter, '_includeNullInQuery'); + + sqlFilter.getSQL(); + + expect(sqlFilter._includeNullInQuery).toHaveBeenCalled(); + }); + }); + + describe('.checkFilters', function () { + let sqlFilter; + + beforeEach(function () { + sqlFilter = new SQLBase(column); + }); + + it('should throw an error when an invalid filter is passed', function () { + expect(function () { + sqlFilter._checkFilters({ unknown_filter: 'filter' }); + }).toThrowError("'unknown_filter' is not a valid filter. Please check documentation."); + }); + + it("should throw an error when there's a type mismatching in filter parameters", function () { + expect(function () { + sqlFilter.ALLOWED_FILTERS = ['in']; + sqlFilter.PARAMETER_SPECIFICATION = { in: PARAMETER_SPECIFICATION.in }; + + sqlFilter._checkFilters({ in: 1 }); + }).toThrowError("Invalid parameter type for 'in'. Please check filters documentation."); + }); }); describe('._convertValueToSQLString', function () { it('should format date to ISO8601 string', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); const fakeDate = new Date('Thu Jun 28 2018 15:04:31 GMT+0200 (Central European Summer Time)'); - expect(sqlFilter._convertValueToSQLString(fakeDate)).toBe('2018-06-28T13:04:31.000Z'); + expect(sqlFilter._convertValueToSQLString(fakeDate)).toBe("'2018-06-28T13:04:31.000Z'"); }); it('should convert array to a comma-separated string wrapped by single comma', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); const fakeArray = ['Element 1', 'Element 2']; @@ -137,14 +193,12 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it('should return number without modifying', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); expect(sqlFilter._convertValueToSQLString(1)).toBe(1); }); it('should return object without modifying', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); const fakeObject = { fakeProperty: 'fakeValue' }; @@ -153,25 +207,23 @@ describe('api/v4/filter/sql/sql-filter-base', function () { }); it('should wrap strings in single-quotes', function () { - const column = 'fake_column'; const sqlFilter = new SQLBase(column); const fakeString = 'fake_string'; expect(sqlFilter._convertValueToSQLString(fakeString)).toBe(`'${fakeString}'`); }); + }); - describe('._interpolateFilterIntoTemplate', function () { - it('should inject filter values into SQL template', function () { - const column = 'fake_column'; - const sqlFilter = new SQLBase(column); + describe('._interpolateFilterIntoTemplate', function () { + it('should inject filter values into SQL template', function () { + const sqlFilter = new SQLBase(column); - sqlFilter.SQL_TEMPLATES = { - 'GTE': '<%= column %> > <%= value %>' - }; + sqlFilter.SQL_TEMPLATES = { + gte: '<%= column %> > <%= value %>' + }; - expect(sqlFilter._interpolateFilterIntoTemplate('GTE', 10)).toBe('fake_column > 10'); - }); + expect(sqlFilter._interpolateFilter('gte', 10)).toBe('fake_column > 10'); }); }); }); diff --git a/test/spec/api/v4/filter/category.spec.js b/test/spec/api/v4/filter/category.spec.js index c145f382c5..5093f1c3be 100644 --- a/test/spec/api/v4/filter/category.spec.js +++ b/test/spec/api/v4/filter/category.spec.js @@ -1,6 +1,6 @@ const carto = require('../../../../../src/api/v4/index'); -describe('api/v4/filter/sql/category', function () { +describe('api/v4/filter/category', function () { describe('constructor', function () { it('should throw a descriptive error when an unknown filter has been passed', function () { expect(function () { diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js new file mode 100644 index 0000000000..e05859f75a --- /dev/null +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -0,0 +1,150 @@ +const FiltersCollection = require('../../../../../src/api/v4/filter/filters-collection'); +const carto = require('../../../../../src/api/v4/index'); + +const column = 'fake_column'; + +describe('api/v4/filter/filters-collection', function () { + describe('constructor', function () { + it('should call _initialize', function () { + spyOn(FiltersCollection.prototype, '_initialize'); + new FiltersCollection(); // eslint-disable-line + + expect(FiltersCollection.prototype._initialize).toHaveBeenCalled(); + }); + }); + + describe('._initialize', function () { + it('should set an empty array to filters', function () { + const filtersCollection = new FiltersCollection(); + expect(filtersCollection._filters).toEqual([]); + }); + + it('should set provided filters and call add()', function () { + spyOn(FiltersCollection.prototype, 'addFilter').and.callThrough(); + + const filters = [ + new carto.filter.Range(column, { lt: 1 }), + new carto.filter.Category(column, { in: ['category'] }) + ]; + const filtersCollection = new FiltersCollection(filters); + + expect(filtersCollection._filters).toEqual(filters); + expect(FiltersCollection.prototype.addFilter).toHaveBeenCalledTimes(2); + }); + }); + + describe('.addFilter', function () { + let filtersCollection, rangeFilter, triggerFilterChangeSpy; + + beforeEach(function () { + triggerFilterChangeSpy = spyOn(FiltersCollection.prototype, '_triggerFilterChange'); + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + }); + + it('should throw an error if filter is not an instance of SQLBase or FiltersCollection', function () { + expect(function () { + filtersCollection.addFilter({}); + }).toThrowError('Filters need to extend from carto.filter.SQLBase. Please use carto.filter.Category or carto.filter.Range.'); + }); + + it('should not readd a filter if it is already added', function () { + filtersCollection.addFilter(rangeFilter); + filtersCollection.addFilter(rangeFilter); + + expect(filtersCollection._filters.length).toBe(1); + expect(triggerFilterChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should add new filter and trigger change:filters event', function () { + filtersCollection.addFilter(rangeFilter); + + expect(filtersCollection._filters.length).toBe(1); + expect(triggerFilterChangeSpy).toHaveBeenCalledTimes(1); + }); + + it('should register listener to change:filters event in the added filter', function () { + spyOn(rangeFilter, 'on'); + + filtersCollection.addFilter(rangeFilter); + + expect(rangeFilter.on).toHaveBeenCalled(); + expect(rangeFilter.on.calls.mostRecent().args[0]).toEqual('change:filters'); + }); + }); + + describe('.removeFilter', function () { + let filtersCollection, rangeFilter, triggerFilterChangeSpy; + + beforeEach(function () { + triggerFilterChangeSpy = spyOn(FiltersCollection.prototype, '_triggerFilterChange'); + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + }); + + it('should not remove the filter if it was not already added', function () { + const removedElement = filtersCollection.removeFilter(rangeFilter); + + expect(removedElement).toBeUndefined(); + expect(triggerFilterChangeSpy).not.toHaveBeenCalled(); + }); + + it('should remove the filter if it was already added', function () { + filtersCollection.addFilter(rangeFilter); + + const removedElement = filtersCollection.removeFilter(rangeFilter)[0]; + + expect(removedElement).toBe(rangeFilter); + expect(triggerFilterChangeSpy).toHaveBeenCalled(); + }); + }); + + describe('.count', function () { + let filtersCollection, rangeFilter; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); + }); + + it('should return filters length', function () { + filtersCollection.addFilter(rangeFilter); + + expect(filtersCollection.count()).toBe(1); + }); + }); + + describe('.getSQL', function () { + let filtersCollection; + + beforeEach(function () { + let rangeFilter = new carto.filter.Range(column, { lt: 1 }); + let categoryFilter = new carto.filter.Category(column, { in: ['category'] }); + + filtersCollection = new FiltersCollection(); + filtersCollection.addFilter(rangeFilter); + filtersCollection.addFilter(categoryFilter); + }); + + it('should build the SQL string and join filters', function () { + expect(filtersCollection.getSQL()).toEqual("fake_column < 1 AND fake_column IN ('category')"); + }); + }); + + describe('._triggerFilterChange', function () { + let filtersCollection; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + }); + + it('should trigger change:filters', function () { + spyOn(filtersCollection, 'trigger'); + + const filters = []; + filtersCollection._triggerFilterChange(filters); + + expect(filtersCollection.trigger).toHaveBeenCalledWith('change:filters', filters); + }); + }); +}); diff --git a/test/spec/api/v4/filter/range.spec.js b/test/spec/api/v4/filter/range.spec.js index 3e3d3dcb41..49cfbb3418 100644 --- a/test/spec/api/v4/filter/range.spec.js +++ b/test/spec/api/v4/filter/range.spec.js @@ -1,6 +1,6 @@ const carto = require('../../../../../src/api/v4/index'); -describe('api/v4/filter/sql/range', function () { +describe('api/v4/filter/range', function () { describe('constructor', function () { it('should throw a descriptive error when an unknown filter has been passed', function () { expect(function () { diff --git a/test/spec/api/v4/source/dataset.spec.js b/test/spec/api/v4/source/dataset.spec.js index 59b429e88a..fb106714f9 100644 --- a/test/spec/api/v4/source/dataset.spec.js +++ b/test/spec/api/v4/source/dataset.spec.js @@ -1,4 +1,5 @@ -var carto = require('../../../../../src/api/v4'); +const Base = require('../../../../../src/api/v4/source/base'); +const carto = require('../../../../../src/api/v4'); describe('api/v4/source/dataset', function () { describe('constructor', function () { @@ -51,6 +52,64 @@ describe('api/v4/source/dataset', function () { }); }); + describe('.getQueryToApply', function () { + let populatedPlacesDataset; + + beforeEach(function () { + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + }) + + it('should return original query if applied filters returns no SQL', function () { + expect(populatedPlacesDataset._getQueryToApply()).toBe('SELECT * from ne_10m_populated_places_simple'); + }); + + it('should return wrapped query if filters are applied', function () { + populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(populatedPlacesDataset._getQueryToApply()).toBe("SELECT * FROM (SELECT * from ne_10m_populated_places_simple) as datasetQuery WHERE fake_column IN ('category')"); + }); + }); + + describe('.addFilter', function () { + let populatedPlacesDataset; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + }); + + it('should call original addFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + + describe('.removeFilter', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + + filter = populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + populatedPlacesDataset.addFilter(filter); + }); + + it('should call original removeFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.removeFilter(filter); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + describe('errors', function () { it('should trigger an error when invalid', function (done) { var client = new carto.Client({ diff --git a/test/spec/api/v4/source/sql.spec.js b/test/spec/api/v4/source/sql.spec.js index 78408482d7..b2c229fe7a 100644 --- a/test/spec/api/v4/source/sql.spec.js +++ b/test/spec/api/v4/source/sql.spec.js @@ -1,4 +1,5 @@ -var carto = require('../../../../../src/api/v4'); +const Base = require('../../../../../src/api/v4/source/base'); +const carto = require('../../../../../src/api/v4'); describe('api/v4/source/sql', function () { var sqlQuery; @@ -141,6 +142,64 @@ describe('api/v4/source/sql', function () { }); }); + describe('.getQueryToApply', function () { + let populatedPlacesSQL; + + beforeEach(function () { + populatedPlacesSQL = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); + spyOn(populatedPlacesSQL, '_updateInternalModelQuery'); + }) + + it('should return original query if applied filters returns no SQL', function () { + expect(populatedPlacesSQL._getQueryToApply()).toBe('SELECT * FROM ne_10m_populated_places_simple'); + }); + + it('should return wrapped query if filters are applied', function () { + populatedPlacesSQL.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(populatedPlacesSQL._getQueryToApply()).toBe("SELECT * FROM (SELECT * FROM ne_10m_populated_places_simple) as originalQuery WHERE fake_column IN ('category')"); + }); + }); + + describe('.addFilter', function () { + let populatedPlacesSQL; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesSQL = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); + spyOn(populatedPlacesSQL, '_updateInternalModelQuery'); + }); + + it('should call original addFilter and _updateInternalModelQuery', function () { + populatedPlacesSQL.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesSQL._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesSQL._getQueryToApply()); + }); + }); + + describe('.removeFilter', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + spyOn(Base.prototype, 'addFilter'); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); + + filter = populatedPlacesDataset.addFilter(new carto.filter.Category('fake_column', { in: ['category'] })); + populatedPlacesDataset.addFilter(filter); + }); + + it('should call original removeFilter and _updateInternalModelQuery', function () { + populatedPlacesDataset.removeFilter(filter); + + expect(Base.prototype.addFilter).toHaveBeenCalled(); + expect(populatedPlacesDataset._updateInternalModelQuery).toHaveBeenCalledWith(populatedPlacesDataset._getQueryToApply()); + }); + }); + describe('errors', function () { it('should trigger an error when invalid', function (done) { var client = new carto.Client({ From 46781e946c2dc9a503d9affd9616e85bf2e8f5a3 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 12:07:58 +0200 Subject: [PATCH 09/35] Update documentation --- src/api/v4/filter/and.js | 26 +++++++++++++++++++ src/api/v4/filter/base-sql.js | 10 +++++++- src/api/v4/filter/category.js | 28 +++++++++++++-------- src/api/v4/filter/or.js | 30 ++++++++++++++++++++++ src/api/v4/filter/range.js | 47 +++++++++++++++++++++++------------ 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index e4c3574fe8..d9e564ed5a 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -1,5 +1,31 @@ const FiltersCollection = require('./filters-collection'); +/** + * AND Filter Group + * SQL and Dataset source filter. + * + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. + * + * This filter will group as many filters as you want and it will add them to the query returning the rows that match ALL the filters to render the visualization. + * + * You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`. + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' }); + * // Create a filter by price, showing only listings lower than or equal to 50€ + * const priceFilter = new carto.filter.Range('price', { lte: 50 }); + * + * // Combine the filters with an AND condition, returning rows that match both filters + * const filterByRoomTypeAndPrice = new carto.filter.AND([ roomTypeFilter, priceFilter ]); + * + * // Add filters to the existing source + * source.addFilter(filterByRoomTypeAndPrice); + * + * @class AND + * @memberof carto.filter + * @api + */ class AND extends FiltersCollection { constructor (filters) { super(filters); diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index a6553bc94d..6f95b2aa1a 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -12,7 +12,6 @@ const DEFAULT_JOIN_OPERATOR = 'AND'; * @class carto.filter.SQLBase * @extends carto.filter.Base * @memberof carto.filter - * @api */ class SQLBase extends Base { /** @@ -34,6 +33,11 @@ class SQLBase extends Base { this._options = options; } + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set + * @param {string} filterValue - The value of the filter + */ set (filterType, filterValue) { const newFilter = { [filterType]: filterValue }; @@ -43,6 +47,10 @@ class SQLBase extends Base { this.trigger('change:filters', newFilter); } + /** + * Set the filter conditions, overriding all the previous ones. + * @param {object} filters - The object containing all the new filters to apply. + */ setFilters (filters) { this._checkFilters(filters); this._filters = filters; diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 4d02956d06..f8aa02b59c 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -13,11 +13,20 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS) /** * Category Filter + * SQL and Dataset source filter. * - * When including this filter into a {@link source.sql} or a {@link source.dataset}, the rows will be filtered by the conditions included within the filter. + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. * - * @class carto.filter.Category - * @extends carto.filter.SQLBase + * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. + * + * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Entire home/apt' }); + * airbnbDataset.addFilter(roomTypeFilter); + * + * @class Category * @memberof carto.filter * @api */ @@ -26,15 +35,14 @@ class Category extends SQLBase { * Create a Category Filter * @param {string} column - The column which the filter will be performed against * @param {object} filters - The filters that you want to apply to the table rows - * @param {string[]} [filters.in] - Filter rows whose column value is included within the provided values - * @param {string[]} [filters.notIn] - Filter rows whose column value is included within the provided values - * @param {(string|number|Date)} [filters.eq] - Filter rows whose column value is equal to the provided value - * @param {(string|number|Date)} [filters.notEq] - Filter rows whose column value is not equal to the provided value - * @param {string} [filters.like] - Filter rows whose column value is like the provided value - * @param {string} [filters.similarTo] - Filter rows whose column value is similar to the provided values + * @param {string[]} filters.in - Return rows whose column value is included within the provided values + * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values + * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value + * @param {(string|number|Date)} filters.notEq - Return rows whose column value is not equal to the provided value + * @param {string} filters.like - Return rows whose column value is like the provided value + * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values * @param {object} [options] * @param {boolean} [options.includeNull] - The operation to apply to the data - * @param {boolean} [options.reverseConditions] - The operation to apply to the data */ constructor (column, filters = {}, options) { super(column, options); diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index abb678b423..80c9cc5430 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -1,6 +1,36 @@ const FiltersCollection = require('./filters-collection'); +/** + * OR Filter Group + * SQL and Dataset source filter. + * + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. + * + * This filter will group as many filters as you want and it will add them to the query returning the rows that match ANY of the filters to render the visualization. + * + * You can add or remove filters by invoking `.addFilter()` and `.removeFilter()`. + * + * @example + * // Create a filter by room type, showing only private rooms + * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Private room' }); + * // Create a filter by price, showing only listings lower than or equal to 50€ + * const priceFilter = new carto.filter.Range('price', { lte: 50 }); + * + * // Combine the filters with an OR operator, returning rows that match one or the other filter + * const filterByRoomTypeOrPrice = new carto.filter.OR([ roomTypeFilter, priceFilter ]); + * + * // Add filters to the existing source + * source.addFilter(filterByRoomTypeOrPrice); + * + * @class OR + * @memberof carto.filter + * @api + */ class OR extends FiltersCollection { + /** + * Create a OR group filter + * @param {Array} filters - The filters to apply in the query + */ constructor (filters) { super(filters); this.JOIN_OPERATOR = 'OR'; diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index 6f9741dbae..de861fb37e 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -35,33 +35,48 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); /** * Range Filter + * SQL and Dataset source filter. * - * When including this filter into a {@link source.sql} or a {@link source.dataset}, the rows will be filtered by the conditions included within filters. + * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. * - * @class carto.filter.Range - * @extends carto.filter.SQLBaseFilter + * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. + * + * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. + * + * @example + * // Create a filter by price, showing only listings lower than or equal to 50€, and higher than 100€ + * const priceFilter = new carto.filter.Range('price', { lte: 50, gt: 100 }); + * + * // Add filter to the existing source + * airbnbDataset.addFilter(priceFilter); + * + * @class Range * @memberof carto.filter * @api */ class Range extends SQLBase { /** * Create a Range filter - * //TODO: poner not between y not between symmetric * @param {string} column - The column to filter rows - * @param {object} [filters] - The filters you want to apply to the column - * @param {(number|Date)} [filters.lt] - Filter rows whose column value is less than the provided value - * @param {(number|Date)} [filters.lte] - Filter rows whose column value is less than or equal to the provided value - * @param {(number|Date)} [filters.gt] - Filter rows whose column value is greater than to the provided value - * @param {(number|Date)} [filters.gte] - Filter rows whose column value is greater than or equal to the provided value - * @param {(number|Date)} [filters.between] - Filter rows whose column value is between the provided values - * @param {(number|Date)} [filters.between.min] - Lowest value of the comparison range - * @param {(number|Date)} [filters.between.max] - Upper value of the comparison range - * @param {(number|Date)} [filters.betweenSymmetric] - Filter rows whose column value is between the provided values after sorting them - * @param {(number|Date)} [filters.betweenSymmetric.min] - Lowest value of the comparison range - * @param {(number|Date)} [filters.betweenSymmetric.max] - Upper value of the comparison range + * @param {object} filters - The filters you want to apply to the column + * @param {(number|Date)} filters.lt - Return rows whose column value is less than the provided value + * @param {(number|Date)} filters.lte - Return rows whose column value is less than or equal to the provided value + * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value + * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value + * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values + * @param {(number|Date)} filters.between.min - Lowest value of the comparison range + * @param {(number|Date)} filters.between.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values + * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them + * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them + * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range * @param {object} [options] * @param {boolean} [options.includeNull] - The operation to apply to the data - * @param {boolean} [options.reverseConditions] - The operation to apply to the data */ constructor (column, filters = {}, options) { super(column, options); From 112be6adaa18cffb34948aa0300a2dc1f214cdcd Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 12:59:34 +0200 Subject: [PATCH 10/35] Test class properties --- src/api/v4/filter/category.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index f8aa02b59c..2e46bd9c23 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -21,6 +21,17 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS) * * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. * + * @param {string} column - The column which the filter will be performed against + * @param {object} filters - The filters that you want to apply to the table rows + * @param {string[]} filters.in - Return rows whose column value is included within the provided values + * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values + * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value + * @param {(string|number|Date)} filters.notEq - Return rows whose column value is not equal to the provided value + * @param {string} filters.like - Return rows whose column value is like the provided value + * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values + * @param {object} [options] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * * @example * // Create a filter by room type, showing only private rooms * const roomTypeFilter = new carto.filter.Category('room_type', { eq: 'Entire home/apt' }); From edac143e160c109165facdad762942c9f5f391ea Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 14:43:43 +0200 Subject: [PATCH 11/35] Update docs --- src/api/v4/filter/and.js | 4 +- src/api/v4/filter/base-sql.js | 16 ++---- src/api/v4/filter/category.js | 36 +++++++----- src/api/v4/filter/filters-collection.js | 4 +- src/api/v4/filter/or.js | 4 +- src/api/v4/filter/range.js | 75 +++++++++++++++++-------- src/api/v4/source/dataset.js | 2 +- src/api/v4/source/sql.js | 2 +- 8 files changed, 88 insertions(+), 55 deletions(-) diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index d9e564ed5a..3d8e13dba0 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -32,8 +32,8 @@ class AND extends FiltersCollection { this.JOIN_OPERATOR = 'AND'; } - getSQL () { - const sql = FiltersCollection.prototype.getSQL.apply(this); + $getSQL () { + const sql = FiltersCollection.prototype.$getSQL.apply(this); if (this.count() > 1) { return `(${sql})`; diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 6f95b2aa1a..25e1f0079c 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -9,19 +9,15 @@ const DEFAULT_JOIN_OPERATOR = 'AND'; * * A SQL filter is the base for all the SQL filters such as the Category Filter or the Range filter * - * @class carto.filter.SQLBase + * @param {string} column - The filtering will be performed against this column + * @param {object} [options={}] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * + * @class SQLBase * @extends carto.filter.Base * @memberof carto.filter */ class SQLBase extends Base { - /** - * Creates an instance of SQLBase. - * @param {string} column - The filtering will be performed against this column - * @param {object} [options={}] - * @param {boolean} [options.includeNull] - The operation to apply to the data - * @param {boolean} [options.reverseConditions] - The operation to apply to the data - * @memberof carto.filter.SQLBase - */ constructor (column, options = {}) { super(); @@ -58,7 +54,7 @@ class SQLBase extends Base { this.trigger('change:filters', filters); } - getSQL () { + $getSQL () { const filters = Object.keys(this._filters); let sql = filters.map(filterType => this._interpolateFilter(filterType, this._filters[filterType])) .join(` ${DEFAULT_JOIN_OPERATOR} `); diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 2e46bd9c23..75755e39f2 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -22,7 +22,7 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS) * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. * * @param {string} column - The column which the filter will be performed against - * @param {object} filters - The filters that you want to apply to the table rows + * @param {object} filters - The filters you want to apply to the table rows * @param {string[]} filters.in - Return rows whose column value is included within the provided values * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value @@ -42,19 +42,6 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS) * @api */ class Category extends SQLBase { - /** - * Create a Category Filter - * @param {string} column - The column which the filter will be performed against - * @param {object} filters - The filters that you want to apply to the table rows - * @param {string[]} filters.in - Return rows whose column value is included within the provided values - * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values - * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value - * @param {(string|number|Date)} filters.notEq - Return rows whose column value is not equal to the provided value - * @param {string} filters.like - Return rows whose column value is like the provided value - * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values - * @param {object} [options] - * @param {boolean} [options.includeNull] - The operation to apply to the data - */ constructor (column, filters = {}, options) { super(column, options); @@ -78,4 +65,25 @@ class Category extends SQLBase { } } +/** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} + * + * @method set + */ + +/** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - The object containing all the new filters to apply. + * @param {string[]} filters.in - Return rows whose column value is included within the provided values + * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values + * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value + * @param {(string|number|Date)} filters.notEq - Return rows whose column value is not equal to the provided value + * @param {string} filters.like - Return rows whose column value is like the provided value + * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values + * + * @method setFilters + */ + module.exports = Category; diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 7bb52c6995..198921dee7 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -42,8 +42,8 @@ class FiltersCollection extends Base { return this._filters.length; } - getSQL () { - return this._filters.map(filter => filter.getSQL()) + $getSQL () { + return this._filters.map(filter => filter.$getSQL()) .join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `); } diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index 80c9cc5430..de27803a56 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -36,8 +36,8 @@ class OR extends FiltersCollection { this.JOIN_OPERATOR = 'OR'; } - getSQL () { - const sql = FiltersCollection.prototype.getSQL.apply(this); + $getSQL () { + const sql = FiltersCollection.prototype.$getSQL.apply(this); if (this.count() > 1) { return `(${sql})`; diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index de861fb37e..dcf7f77216 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -43,6 +43,27 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); * * This filter won't include null values within returned rows by default but you can include them by setting `includeNull` option. * + * @param {string} column - The column to filter rows + * @param {object} filters - The filters you want to apply to the column + * @param {(number|Date)} filters.lt - Return rows whose column value is less than the provided value + * @param {(number|Date)} filters.lte - Return rows whose column value is less than or equal to the provided value + * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value + * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value + * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values + * @param {(number|Date)} filters.between.min - Lowest value of the comparison range + * @param {(number|Date)} filters.between.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values + * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them + * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them + * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range + * @param {object} [options] + * @param {boolean} [options.includeNull] - The operation to apply to the data + * * @example * // Create a filter by price, showing only listings lower than or equal to 50€, and higher than 100€ * const priceFilter = new carto.filter.Range('price', { lte: 50, gt: 100 }); @@ -55,29 +76,6 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); * @api */ class Range extends SQLBase { - /** - * Create a Range filter - * @param {string} column - The column to filter rows - * @param {object} filters - The filters you want to apply to the column - * @param {(number|Date)} filters.lt - Return rows whose column value is less than the provided value - * @param {(number|Date)} filters.lte - Return rows whose column value is less than or equal to the provided value - * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value - * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value - * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values - * @param {(number|Date)} filters.between.min - Lowest value of the comparison range - * @param {(number|Date)} filters.between.max - Upper value of the comparison range - * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values - * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range - * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range - * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them - * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range - * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range - * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them - * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range - * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range - * @param {object} [options] - * @param {boolean} [options.includeNull] - The operation to apply to the data - */ constructor (column, filters = {}, options) { super(column, options); @@ -103,4 +101,35 @@ class Range extends SQLBase { } } +/** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} + * + * @method set + */ + +/** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - Object containing all the new filters to apply. + * @param {(number|Date)} filters.lt - Return rows whose column value is less than the provided value + * @param {(number|Date)} filters.lte - Return rows whose column value is less than or equal to the provided value + * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value + * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value + * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values + * @param {(number|Date)} filters.between.min - Lowest value of the comparison range + * @param {(number|Date)} filters.between.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values + * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them + * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them + * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range + * + * @method setFilters + */ + module.exports = Range; diff --git a/src/api/v4/source/dataset.js b/src/api/v4/source/dataset.js index f71d9ad6bc..4c3f7164af 100644 --- a/src/api/v4/source/dataset.js +++ b/src/api/v4/source/dataset.js @@ -64,7 +64,7 @@ Dataset.prototype._updateInternalModelQuery = function (query) { }; Dataset.prototype._getQueryToApply = function () { - const whereClause = this._appliedFilters.getSQL(); + const whereClause = this._appliedFilters.$getSQL(); const datasetQuery = `SELECT * from ${this._tableName}`; if (_.isEmpty(whereClause)) { diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js index bdada9b4c1..6b5ccbf62a 100644 --- a/src/api/v4/source/sql.js +++ b/src/api/v4/source/sql.js @@ -91,7 +91,7 @@ SQL.prototype._updateInternalModelQuery = function (query) { }; SQL.prototype._getQueryToApply = function () { - const whereClause = this._appliedFilters.getSQL(); + const whereClause = this._appliedFilters.$getSQL(); if (!this._hasFiltersApplied || _.isEmpty(whereClause)) { return this._query; From 6a6192fa1fa3f373f3923237cf0ce73779b6b68b Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 15:35:37 +0200 Subject: [PATCH 12/35] Update docs --- src/api/v4/filter/and.js | 1 + src/api/v4/filter/filters-collection.js | 31 +++++++++++++++++++++++++ src/api/v4/filter/or.js | 1 + 3 files changed, 33 insertions(+) diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index 3d8e13dba0..80f9ed2eba 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -23,6 +23,7 @@ const FiltersCollection = require('./filters-collection'); * source.addFilter(filterByRoomTypeAndPrice); * * @class AND + * @extends carto.filter.FiltersCollection * @memberof carto.filter * @api */ diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 198921dee7..d268e06b54 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -4,6 +4,18 @@ const SQLBase = require('./base-sql'); const DEFAULT_JOIN_OPERATOR = 'AND'; +/** + * Filters Collection + * Base class for AND and OR filters. + * + * Filters Collection is a way to group a set of filters in order to create composed filters, allowing the user to change the operator that joins the filters. + * + * **This object should not be used directly** + * + * @class FiltersCollection + * @memberof carto.filter + * @api + */ class FiltersCollection extends Base { constructor (filters) { super(); @@ -18,6 +30,12 @@ class FiltersCollection extends Base { } } + /** + * Add a new filter to the collection + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @memberof FiltersCollection + */ addFilter (filter) { if (!(filter instanceof SQLBase) && !(filter instanceof FiltersCollection)) { throw this._getValidationError('wrongFilterType'); @@ -30,6 +48,13 @@ class FiltersCollection extends Base { this._triggerFilterChange(); } + /** + * Remove an existing filter from the collection + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * @returns {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} The removed element + * @memberof FiltersCollection + */ removeFilter (filter) { if (!_.contains(this._filters, filter)) return; @@ -38,6 +63,12 @@ class FiltersCollection extends Base { return removedElement; } + /** + * Get the number of added filters to the collection + * + * @returns {number} Number of contained filters + * @memberof FiltersCollection + */ count () { return this._filters.length; } diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index de27803a56..5709f1f1e0 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -23,6 +23,7 @@ const FiltersCollection = require('./filters-collection'); * source.addFilter(filterByRoomTypeOrPrice); * * @class OR + * @extends carto.filter.FiltersCollection * @memberof carto.filter * @api */ From 4416e302a5c6b7b19bf1042308904c5ed28e15ae Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 2 Jul 2018 17:26:06 +0200 Subject: [PATCH 13/35] Add API to FiltersCollection methods --- src/api/v4/filter/and.js | 3 ++- src/api/v4/filter/category.js | 5 ++++- src/api/v4/filter/filters-collection.js | 3 +++ src/api/v4/filter/or.js | 3 ++- src/api/v4/filter/range.js | 13 ++++++++----- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index 80f9ed2eba..9650479535 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -1,7 +1,8 @@ const FiltersCollection = require('./filters-collection'); /** - * AND Filter Group + * AND Filter Group. + * * SQL and Dataset source filter. * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 75755e39f2..80aae49682 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -12,7 +12,8 @@ const CATEGORY_COMPARISON_OPERATORS = { const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS)); /** - * Category Filter + * Category Filter. + * * SQL and Dataset source filter. * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. @@ -71,6 +72,7 @@ class Category extends SQLBase { * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} * * @method set + * @api */ /** @@ -84,6 +86,7 @@ class Category extends SQLBase { * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values * * @method setFilters + * @api */ module.exports = Category; diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index d268e06b54..4475c7f289 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -35,6 +35,7 @@ class FiltersCollection extends Base { * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * @memberof FiltersCollection + * @api */ addFilter (filter) { if (!(filter instanceof SQLBase) && !(filter instanceof FiltersCollection)) { @@ -54,6 +55,7 @@ class FiltersCollection extends Base { * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * @returns {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} The removed element * @memberof FiltersCollection + * @api */ removeFilter (filter) { if (!_.contains(this._filters, filter)) return; @@ -68,6 +70,7 @@ class FiltersCollection extends Base { * * @returns {number} Number of contained filters * @memberof FiltersCollection + * @api */ count () { return this._filters.length; diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index 5709f1f1e0..3f23a70432 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -1,7 +1,8 @@ const FiltersCollection = require('./filters-collection'); /** - * OR Filter Group + * OR Filter Group. + * * SQL and Dataset source filter. * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index dcf7f77216..455e44d36a 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -34,7 +34,8 @@ const RANGE_COMPARISON_OPERATORS = { const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); /** - * Range Filter + * Range Filter. + * * SQL and Dataset source filter. * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. @@ -50,16 +51,16 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values - * @param {(number|Date)} filters.between.min - Lowest value of the comparison range + * @param {(number|Date)} filters.between.min - Lower value of the comparison range * @param {(number|Date)} filters.between.max - Upper value of the comparison range * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values - * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetween.min - Lower value of the comparison range * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them - * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.betweenSymmetric.min - Lower value of the comparison range * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them - * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range + * @param {(number|Date)} filters.notBetweenSymmetric.min - Lower value of the comparison range * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range * @param {object} [options] * @param {boolean} [options.includeNull] - The operation to apply to the data @@ -107,6 +108,7 @@ class Range extends SQLBase { * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} * * @method set + * @api */ /** @@ -130,6 +132,7 @@ class Range extends SQLBase { * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range * * @method setFilters + * @api */ module.exports = Range; From f1168c3edafa66ca9809f39b6137513707777f73 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 3 Jul 2018 12:26:18 +0200 Subject: [PATCH 14/35] Set correct parameter specification --- src/api/v4/filter/base-sql.js | 2 +- src/api/v4/filter/filters-collection.js | 5 +- src/api/v4/filter/range.js | 64 +++++++------------ src/api/v4/source/sql.js | 2 + test/spec/api/v4/filter/base-sql.spec.js | 6 +- test/spec/api/v4/filter/category.spec.js | 12 ++-- .../api/v4/filter/filters-collection.spec.js | 5 +- test/spec/api/v4/filter/range.spec.js | 16 ++--- 8 files changed, 50 insertions(+), 62 deletions(-) diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 25e1f0079c..b1178f5f8e 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -91,7 +91,7 @@ class SQLBase extends Base { const parameters = this.PARAMETER_SPECIFICATION[filter].parameters; const haveCorrectType = parameters.every( parameter => { - const parameterValue = _.property(parameter.name)(filters[filter]) || filters[parameter.name]; + const parameterValue = _.property(parameter.name)(filters); return parameter.allowedTypes.some(type => parameterIsOfType(type, parameterValue)); } ); diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 4475c7f289..0b2baf21cc 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -5,12 +5,13 @@ const SQLBase = require('./base-sql'); const DEFAULT_JOIN_OPERATOR = 'AND'; /** - * Filters Collection + * Filters Collection. + * * Base class for AND and OR filters. * * Filters Collection is a way to group a set of filters in order to create composed filters, allowing the user to change the operator that joins the filters. * - * **This object should not be used directly** + * **This object should not be used directly.** * * @class FiltersCollection * @memberof carto.filter diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index 455e44d36a..0924a7066c 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -7,26 +7,26 @@ const RANGE_COMPARISON_OPERATORS = { gte: { parameters: [{ name: 'gte', allowedTypes: ['Number', 'Date'] }] }, between: { parameters: [ - { name: 'min', allowedTypes: ['Number', 'Date'] }, - { name: 'max', allowedTypes: ['Number', 'Date'] } + { name: 'between.min', allowedTypes: ['Number', 'Date'] }, + { name: 'between.max', allowedTypes: ['Number', 'Date'] } ] }, notBetween: { parameters: [ - { name: 'min', allowedTypes: ['Number', 'Date'] }, - { name: 'max', allowedTypes: ['Number', 'Date'] } + { name: 'notBetween.min', allowedTypes: ['Number', 'Date'] }, + { name: 'notBetween.max', allowedTypes: ['Number', 'Date'] } ] }, betweenSymmetric: { parameters: [ - { name: 'min', allowedTypes: ['Number', 'Date'] }, - { name: 'max', allowedTypes: ['Number', 'Date'] } + { name: 'betweenSymmetric.min', allowedTypes: ['Number', 'Date'] }, + { name: 'betweenSymmetric.max', allowedTypes: ['Number', 'Date'] } ] }, notBetweenSymmetric: { parameters: [ - { name: 'min', allowedTypes: ['Number', 'Date'] }, - { name: 'max', allowedTypes: ['Number', 'Date'] } + { name: 'notBetweenSymmetric.min', allowedTypes: ['Number', 'Date'] }, + { name: 'notBetweenSymmetric.max', allowedTypes: ['Number', 'Date'] } ] } }; @@ -100,39 +100,23 @@ class Range extends SQLBase { notBetweenSymmetric: '<%= column %> NOT BETWEEN SYMMETRIC <%= value.min %> AND <%= value.max %>' }; } -} -/** - * Set any of the filter conditions, overwriting the previous one. - * @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`. - * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} - * - * @method set - * @api - */ + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} + * + * @method set + * @api + */ -/** - * Set filter conditions, overriding all the previous ones. - * @param {object} filters - Object containing all the new filters to apply. - * @param {(number|Date)} filters.lt - Return rows whose column value is less than the provided value - * @param {(number|Date)} filters.lte - Return rows whose column value is less than or equal to the provided value - * @param {(number|Date)} filters.gt - Return rows whose column value is greater than to the provided value - * @param {(number|Date)} filters.gte - Return rows whose column value is greater than or equal to the provided value - * @param {(number|Date)} filters.between - Return rows whose column value is between the provided values - * @param {(number|Date)} filters.between.min - Lowest value of the comparison range - * @param {(number|Date)} filters.between.max - Upper value of the comparison range - * @param {(number|Date)} filters.notBetween - Return rows whose column value is not between the provided values - * @param {(number|Date)} filters.notBetween.min - Lowest value of the comparison range - * @param {(number|Date)} filters.notBetween.max - Upper value of the comparison range - * @param {(number|Date)} filters.betweenSymmetric - Return rows whose column value is between the provided values after sorting them - * @param {(number|Date)} filters.betweenSymmetric.min - Lowest value of the comparison range - * @param {(number|Date)} filters.betweenSymmetric.max - Upper value of the comparison range - * @param {(number|Date)} filters.notBetweenSymmetric - Return rows whose column value is not between the provided values after sorting them - * @param {(number|Date)} filters.notBetweenSymmetric.min - Lowest value of the comparison range - * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range - * - * @method setFilters - * @api - */ + /** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - Object containing all the new filters to apply. Check filter options above. + * + * @method setFilters + * @api + */ +} module.exports = Range; diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js index 6b5ccbf62a..4c1f799b40 100644 --- a/src/api/v4/source/sql.js +++ b/src/api/v4/source/sql.js @@ -83,6 +83,8 @@ SQL.prototype._createInternalModel = function (engine) { }; SQL.prototype._updateInternalModelQuery = function (query) { + if (!this._internalModel) return; + this._internalModel.set('query', query, { silent: true }); return this._internalModel._engine.reload() diff --git a/test/spec/api/v4/filter/base-sql.spec.js b/test/spec/api/v4/filter/base-sql.spec.js index 3829ff0a60..e6a7240472 100644 --- a/test/spec/api/v4/filter/base-sql.spec.js +++ b/test/spec/api/v4/filter/base-sql.spec.js @@ -122,7 +122,7 @@ describe('api/v4/filter/base-sql', function () { }); }); - describe('.getSQL', function () { + describe('.$getSQL', function () { it('should return SQL string containing all the filters joined by AND clause', function () { const sqlFilter = new SQLBase(column); sqlFilter.ALLOWED_FILTERS = ['in', 'like']; @@ -136,7 +136,7 @@ describe('api/v4/filter/base-sql', function () { }; sqlFilter.setFilters({ in: ['category 1', 'category 2'], like: '%category%' }); - expect(sqlFilter.getSQL()).toBe("fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%'"); + expect(sqlFilter.$getSQL()).toBe("fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%'"); }); it('should call _includeNullInQuery if includeNull option is set', function () { @@ -148,7 +148,7 @@ describe('api/v4/filter/base-sql', function () { spyOn(sqlFilter, '_includeNullInQuery'); - sqlFilter.getSQL(); + sqlFilter.$getSQL(); expect(sqlFilter._includeNullInQuery).toHaveBeenCalled(); }); diff --git a/test/spec/api/v4/filter/category.spec.js b/test/spec/api/v4/filter/category.spec.js index 5093f1c3be..d789acfeca 100644 --- a/test/spec/api/v4/filter/category.spec.js +++ b/test/spec/api/v4/filter/category.spec.js @@ -12,32 +12,32 @@ describe('api/v4/filter/category', function () { describe('SQL Templates', function () { it('IN', function () { const categoryFilter = new carto.filter.Category('fake_column', { in: ['Category 1'] }); - expect(categoryFilter.getSQL()).toBe("fake_column IN ('Category 1')"); + expect(categoryFilter.$getSQL()).toBe("fake_column IN ('Category 1')"); }); it('NOT IN', function () { const categoryFilter = new carto.filter.Category('fake_column', { notIn: ['Category 1'] }); - expect(categoryFilter.getSQL()).toBe("fake_column NOT IN ('Category 1')"); + expect(categoryFilter.$getSQL()).toBe("fake_column NOT IN ('Category 1')"); }); it('EQ', function () { const categoryFilter = new carto.filter.Category('fake_column', { eq: 'Category 1' }); - expect(categoryFilter.getSQL()).toBe("fake_column = 'Category 1'"); + expect(categoryFilter.$getSQL()).toBe("fake_column = 'Category 1'"); }); it('NOT EQ', function () { const categoryFilter = new carto.filter.Category('fake_column', { notEq: 'Category 1' }); - expect(categoryFilter.getSQL()).toBe("fake_column != 'Category 1'"); + expect(categoryFilter.$getSQL()).toBe("fake_column != 'Category 1'"); }); it('LIKE', function () { const categoryFilter = new carto.filter.Category('fake_column', { like: '%Category%' }); - expect(categoryFilter.getSQL()).toBe("fake_column LIKE '%Category%'"); + expect(categoryFilter.$getSQL()).toBe("fake_column LIKE '%Category%'"); }); it('SIMILAR TO', function () { const categoryFilter = new carto.filter.Category('fake_column', { similarTo: '%Category%' }); - expect(categoryFilter.getSQL()).toBe("fake_column SIMILAR TO '%Category%'"); + expect(categoryFilter.$getSQL()).toBe("fake_column SIMILAR TO '%Category%'"); }); }); }); diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js index e05859f75a..9440e02eb4 100644 --- a/test/spec/api/v4/filter/filters-collection.spec.js +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -21,6 +21,7 @@ describe('api/v4/filter/filters-collection', function () { it('should set provided filters and call add()', function () { spyOn(FiltersCollection.prototype, 'addFilter').and.callThrough(); + debugger; const filters = [ new carto.filter.Range(column, { lt: 1 }), @@ -114,7 +115,7 @@ describe('api/v4/filter/filters-collection', function () { }); }); - describe('.getSQL', function () { + describe('.$getSQL', function () { let filtersCollection; beforeEach(function () { @@ -127,7 +128,7 @@ describe('api/v4/filter/filters-collection', function () { }); it('should build the SQL string and join filters', function () { - expect(filtersCollection.getSQL()).toEqual("fake_column < 1 AND fake_column IN ('category')"); + expect(filtersCollection.$getSQL()).toEqual("fake_column < 1 AND fake_column IN ('category')"); }); }); diff --git a/test/spec/api/v4/filter/range.spec.js b/test/spec/api/v4/filter/range.spec.js index 49cfbb3418..ed365c7479 100644 --- a/test/spec/api/v4/filter/range.spec.js +++ b/test/spec/api/v4/filter/range.spec.js @@ -12,42 +12,42 @@ describe('api/v4/filter/range', function () { describe('SQL Templates', function () { it('LT', function () { const categoryFilter = new carto.filter.Range('fake_column', { lt: 10 }); - expect(categoryFilter.getSQL()).toBe('fake_column < 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column < 10'); }); it('LTE', function () { const categoryFilter = new carto.filter.Range('fake_column', { lte: 10 }); - expect(categoryFilter.getSQL()).toBe('fake_column <= 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column <= 10'); }); it('GT', function () { const categoryFilter = new carto.filter.Range('fake_column', { gt: 10 }); - expect(categoryFilter.getSQL()).toBe('fake_column > 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column > 10'); }); it('GTE', function () { const categoryFilter = new carto.filter.Range('fake_column', { gte: 10 }); - expect(categoryFilter.getSQL()).toBe('fake_column >= 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column >= 10'); }); it('BETWEEN', function () { const categoryFilter = new carto.filter.Range('fake_column', { between: { min: 1, max: 10 } }); - expect(categoryFilter.getSQL()).toBe('fake_column BETWEEN 1 AND 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column BETWEEN 1 AND 10'); }); it('NOT BETWEEN', function () { const categoryFilter = new carto.filter.Range('fake_column', { notBetween: { min: 1, max: 10 } }); - expect(categoryFilter.getSQL()).toBe('fake_column NOT BETWEEN 1 AND 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column NOT BETWEEN 1 AND 10'); }); it('BETWEEN SYMMETRIC', function () { const categoryFilter = new carto.filter.Range('fake_column', { betweenSymmetric: { min: 1, max: 10 } }); - expect(categoryFilter.getSQL()).toBe('fake_column BETWEEN SYMMETRIC 1 AND 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column BETWEEN SYMMETRIC 1 AND 10'); }); it('NOT BETWEEN SYMMETRIC', function () { const categoryFilter = new carto.filter.Range('fake_column', { notBetweenSymmetric: { min: 1, max: 10 } }); - expect(categoryFilter.getSQL()).toBe('fake_column NOT BETWEEN SYMMETRIC 1 AND 10'); + expect(categoryFilter.$getSQL()).toBe('fake_column NOT BETWEEN SYMMETRIC 1 AND 10'); }); }); }); From 83f04745bca6cdcc794ac093c665519110bcc200 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 3 Jul 2018 14:56:07 +0200 Subject: [PATCH 15/35] Do not include filters when SQL string is empty. Hack IN operator to discard all rows if empty array is passed. --- src/api/v4/filter/base-sql.js | 4 +++- src/api/v4/filter/category.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index b1178f5f8e..6a43e4ecc4 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -56,7 +56,9 @@ class SQLBase extends Base { $getSQL () { const filters = Object.keys(this._filters); - let sql = filters.map(filterType => this._interpolateFilter(filterType, this._filters[filterType])) + let sql = filters + .map(filterType => this._interpolateFilter(filterType, this._filters[filterType])) + .filter(filter => Boolean(filter)) .join(` ${DEFAULT_JOIN_OPERATOR} `); if (this._options.includeNull) { diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 80aae49682..e88a0d69fe 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -56,8 +56,8 @@ class Category extends SQLBase { _getSQLTemplates () { return { - in: '<%= column %> IN (<%= value %>)', - notIn: '<%= column %> NOT IN (<%= value %>)', + in: '<% if (value) { %><%= column %> IN (<%= value %>)<% } else { %>true = false<% } %>', + notIn: '<% if (value) { %><%= column %> NOT IN (<%= value %>)<% } %>', eq: '<%= column %> = <%= value %>', notEq: '<%= column %> != <%= value %>', like: '<%= column %> LIKE <%= value %>', From 25e7fc5dfdf3fed477cdfa349072efe695841222 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 3 Jul 2018 15:15:33 +0200 Subject: [PATCH 16/35] Category filter example --- examples/public/filters/category-filter.html | 104 +++++++++++++++++++ examples/public/style.css | 3 + 2 files changed, 107 insertions(+) create mode 100644 examples/public/filters/category-filter.html diff --git a/examples/public/filters/category-filter.html b/examples/public/filters/category-filter.html new file mode 100644 index 0000000000..559b5dae39 --- /dev/null +++ b/examples/public/filters/category-filter.html @@ -0,0 +1,104 @@ + + + + Category Filter | CARTO + + + + + + + + + + + +
+ + + + + diff --git a/examples/public/style.css b/examples/public/style.css index 098519a9cf..3d35843ed3 100644 --- a/examples/public/style.css +++ b/examples/public/style.css @@ -167,6 +167,9 @@ aside.toolbox { #controls li input { margin: 0 8px 0 0; } +#controls li input[type=checkbox] { + margin: 2px 8px 0 0; +} #controls li label { font: 12px/16px 'Open Sans'; cursor: pointer; From 2ccf062381c71ef603cfd27909aba6f4d092bf9b Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 3 Jul 2018 19:06:21 +0200 Subject: [PATCH 17/35] Add new examples --- examples/examples.json | 21 +++ examples/public/filters/category-filter.html | 5 + examples/public/filters/range-filter.html | 103 ++++++++++++ examples/public/style.css | 164 +++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 examples/public/filters/range-filter.html diff --git a/examples/examples.json b/examples/examples.json index ae9c06d4f2..695c64a1cf 100644 --- a/examples/examples.json +++ b/examples/examples.json @@ -126,6 +126,27 @@ } ] }, + { + "title": "Filters", + "samples": [ + { + "title": "Category Filter", + "desc": "Create a Category filter with checkbox selectors", + "file": { + "leaflet": "public/filters/category-filter.html", + "gmaps": "public/filters/category-filter.html" + } + }, + { + "title": "Range Filter", + "desc": "Create a Range filter with a range slider", + "file": { + "leaflet": "public/filters/range-filter.html", + "gmaps": "public/filters/range-filter.html" + } + } + ] + }, { "title": "Misc", "samples": [ diff --git a/examples/public/filters/category-filter.html b/examples/public/filters/category-filter.html index 559b5dae39..09fe6b40e4 100644 --- a/examples/public/filters/category-filter.html +++ b/examples/public/filters/category-filter.html @@ -29,6 +29,9 @@

Category Filter

+
+

Room Types

+
  • @@ -60,6 +63,8 @@

    Category Filter

    function applyFilters () { roomTypeFilter.setFilters({ in: getSelectedRoomTypes() }); + // or + // roomTypeFilter.set('in', getSelectedRoomTypes()); } function registerListeners () { diff --git a/examples/public/filters/range-filter.html b/examples/public/filters/range-filter.html new file mode 100644 index 0000000000..76e531a091 --- /dev/null +++ b/examples/public/filters/range-filter.html @@ -0,0 +1,103 @@ + + + + Range Filter | CARTO + + + + + + + + + + + + + +
    + + + + + diff --git a/examples/public/style.css b/examples/public/style.css index 3d35843ed3..e7d9149d86 100644 --- a/examples/public/style.css +++ b/examples/public/style.css @@ -392,3 +392,167 @@ aside.toolbox { border-radius: 0 4px 4px 0; border-right: 1px solid rgba(22, 39, 69, 0.2); } + +/* RANGE SLIDER */ +/* input[type="range"] { + position: relative; + display: inline-block; + width: 100%; + height: 10px; + background-color: rgb(23, 133, 251); + border-radius: 10px; + outline: none; + cursor: pointer; + margin: 0 0 22px; + -webkit-appearance: none; +} + +input[type=range]::before, +input[type=range]::after { + color: #747D82; + position: absolute; + top: 20px; +} + +input[type=range]::before { + content: attr(min-in-currency); + left: 0; +} + +input[type=range]::after { + content: attr(max-in-currency); + right: 0; +} + +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb, +input[type="range"]::-moz-range-thumb, +input[type="range"]::-ms-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-moz-range-track { + height: 10px; + background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); + border-radius: 10px; +} + + +input[type="range"]::-webkit-slider-runnable-track { + width: 100%; +} + +input[type="range"]::-ms-track { + height: 10px; + background-color: rgb(23, 133, 251); +} + +input[type="range"]::-ms-fill-lower { + background-color: rgb(226, 230, 227); +} + +input[type="range"]::-ms-fill-upper { + background-color: rgb(23, 133, 251); +} */ + +input[type="range"] { + position: relative; + display: inline-block; + width: 100%; + height: 10px; + background-color: rgb(23, 133, 251); + border-radius: 10px; + outline: none; + cursor: pointer; + margin: 0 0 22px; + -webkit-appearance: none; +} + +input[type=range]::before, +input[type=range]::after { + color: #747D82; + position: absolute; + top: 20px; +} + +input[type=range]::before { + content: attr(min-with-suffix); + left: 0; +} + +input[type=range]::after { + content: attr(max-with-suffix); + right: 0; +} + +input[type="range"]::-webkit-slider-runnable-track, +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-moz-range-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-ms-thumb { + width: 15px; + height: 15px; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); + margin-top: -2px; +} + +input[type="range"]::-webkit-slider-runnable-track { + height: 10px; + width: 100%; + background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); + border-radius: 10px; +} + +input[type="range"]::-moz-range-track { + height: 10px; + background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); + border-radius: 10px; +} + +input[type="range"]::-ms-track { + background-color: rgb(23, 133, 251); + height: 10px; +} + +input[type="range"]::-ms-fill-lower { + background-color: rgb(226, 230, 227); +} + +input[type="range"]::-ms-fill-upper { + background-color: rgb(23, 133, 251);; +} From 839ae0090f4466592c640953fe9a5f06349ad3a5 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 3 Jul 2018 19:06:35 +0200 Subject: [PATCH 18/35] Change documentation to add category and range methods --- src/api/v4/filter/category.js | 10 +++------- src/api/v4/filter/range.js | 4 +++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index e88a0d69fe..7140c7f617 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -71,20 +71,16 @@ class Category extends SQLBase { * @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`. * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} * + * @memberof Category * @method set * @api */ /** * Set filter conditions, overriding all the previous ones. - * @param {object} filters - The object containing all the new filters to apply. - * @param {string[]} filters.in - Return rows whose column value is included within the provided values - * @param {string[]} filters.notIn - Return rows whose column value is included within the provided values - * @param {(string|number|Date)} filters.eq - Return rows whose column value is equal to the provided value - * @param {(string|number|Date)} filters.notEq - Return rows whose column value is not equal to the provided value - * @param {string} filters.like - Return rows whose column value is like the provided value - * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values +* @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. * + * @memberof Category * @method setFilters * @api */ diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index 0924a7066c..b21f61e08d 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -106,14 +106,16 @@ class Range extends SQLBase { * @param {string} filterType - The filter type that you want to set. `lt`, `lte`, `gt`, `gte`, `between`, `notBetween`, `betweenSymmetric`, `notBetweenSymmetric`. * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Range} * + * @memberof Range * @method set * @api */ /** * Set filter conditions, overriding all the previous ones. - * @param {object} filters - Object containing all the new filters to apply. Check filter options above. + * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Range}. * + * @memberof Range * @method setFilters * @api */ From 0808c6deb92fa09e982735bbfb4ff974f5b2c75a Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 11:04:50 +0200 Subject: [PATCH 19/35] Add AND example. Avoid errors on bad function parameters. Move Category filter methods documentation inside class. --- examples/public/filters/and-filter.html | 147 ++++++++++++++++++++++ examples/public/filters/range-filter.html | 2 - src/api/v4/filter/base-sql.js | 16 ++- src/api/v4/filter/category.js | 36 +++--- 4 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 examples/public/filters/and-filter.html diff --git a/examples/public/filters/and-filter.html b/examples/public/filters/and-filter.html new file mode 100644 index 0000000000..8a567df726 --- /dev/null +++ b/examples/public/filters/and-filter.html @@ -0,0 +1,147 @@ + + + + AND Filter | CARTO + + + + + + + + + + + +
    + + + + + diff --git a/examples/public/filters/range-filter.html b/examples/public/filters/range-filter.html index 76e531a091..90846cec20 100644 --- a/examples/public/filters/range-filter.html +++ b/examples/public/filters/range-filter.html @@ -11,8 +11,6 @@ - -
    diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 6a43e4ecc4..1976e256be 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -35,6 +35,10 @@ class SQLBase extends Base { * @param {string} filterValue - The value of the filter */ set (filterType, filterValue) { + if (!filterType || !filterValue || !_.isString(filterType) || !_.isString(filterValue)) { + return; + } + const newFilter = { [filterType]: filterValue }; this._checkFilters(newFilter); @@ -48,6 +52,10 @@ class SQLBase extends Base { * @param {object} filters - The object containing all the new filters to apply. */ setFilters (filters) { + if (!filters || !_.isObject(filters) || _.isEmpty(filters)) { + return; + } + this._checkFilters(filters); this._filters = filters; @@ -121,7 +129,7 @@ class SQLBase extends Base { if (_.isArray(filterValue)) { return filterValue - .map(value => `'${value.toString()}'`) + .map(value => `'${normalizeString(value.toString())}'`) .join(','); } @@ -129,7 +137,7 @@ class SQLBase extends Base { return filterValue; } - return `'${filterValue.toString()}'`; + return `'${normalizeString(filterValue.toString())}'`; } _interpolateFilter (filterType, filterValue) { @@ -152,4 +160,8 @@ const parameterIsOfType = function (parameterType, parameterValue) { return _[`is${parameterType}`](parameterValue); }; +const normalizeString = function (value) { + return value.replace(/\n/g, '\\n').replace(/\"/g, '\\"').replace(/'/g, "''"); +}; + module.exports = SQLBase; diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 7140c7f617..e810707121 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -64,25 +64,25 @@ class Category extends SQLBase { similarTo: '<%= column %> SIMILAR TO <%= value %>' }; } -} -/** - * Set any of the filter conditions, overwriting the previous one. - * @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`. - * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} - * - * @memberof Category - * @method set - * @api - */ + /** + * Set any of the filter conditions, overwriting the previous one. + * @param {string} filterType - The filter type that you want to set. `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo`. + * @param {string} filterValue - The value of the filter. Check types in {@link carto.filter.Category} + * + * @memberof Category + * @method set + * @api + */ -/** - * Set filter conditions, overriding all the previous ones. -* @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. - * - * @memberof Category - * @method setFilters - * @api - */ + /** + * Set filter conditions, overriding all the previous ones. + * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. + * + * @memberof Category + * @method setFilters + * @api + */ +} module.exports = Category; From 15d0d566c7d7d17fa2764ba38f0123c33ce5e259 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 14:33:20 +0200 Subject: [PATCH 20/35] Complex filters example. Bugfixes. Move code to base class. --- examples/examples.json | 16 ++++ examples/public/filters/complex-filter.html | 87 +++++++++++++++++++++ src/api/v4/filter/and.js | 10 --- src/api/v4/filter/base-sql.js | 4 + src/api/v4/filter/filters-collection.js | 8 +- src/api/v4/filter/or.js | 10 --- src/api/v4/source/dataset.js | 4 +- src/api/v4/source/sql.js | 2 +- 8 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 examples/public/filters/complex-filter.html diff --git a/examples/examples.json b/examples/examples.json index 695c64a1cf..6d6887558e 100644 --- a/examples/examples.json +++ b/examples/examples.json @@ -144,6 +144,22 @@ "leaflet": "public/filters/range-filter.html", "gmaps": "public/filters/range-filter.html" } + }, + { + "title": "AND Filter", + "desc": "Apply an AND filter to combine multiple filters", + "file": { + "leaflet": "public/filters/and-filter.html", + "gmaps": "public/filters/and-filter.html" + } + }, + { + "title": "Complex Filter", + "desc": "Apply several combined filters to a dataset", + "file": { + "leaflet": "public/filters/complex-filter.html", + "gmaps": "public/filters/complex-filter.html" + } } ] }, diff --git a/examples/public/filters/complex-filter.html b/examples/public/filters/complex-filter.html new file mode 100644 index 0000000000..da776d4f48 --- /dev/null +++ b/examples/public/filters/complex-filter.html @@ -0,0 +1,87 @@ + + + + Complex Filter Example | CARTO + + + + + + + + + + + +
    + + + + + diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index 9650479535..60c061749e 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -33,16 +33,6 @@ class AND extends FiltersCollection { super(filters); this.JOIN_OPERATOR = 'AND'; } - - $getSQL () { - const sql = FiltersCollection.prototype.$getSQL.apply(this); - - if (this.count() > 1) { - return `(${sql})`; - } - - return sql; - } } module.exports = AND; diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 1976e256be..38112200d0 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -73,6 +73,10 @@ class SQLBase extends Base { this._includeNullInQuery(sql); } + if (filters.length > 1) { + return `(${sql})`; + } + return sql; } diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 0b2baf21cc..6037f02b7f 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -78,8 +78,14 @@ class FiltersCollection extends Base { } $getSQL () { - return this._filters.map(filter => filter.$getSQL()) + const sql = this._filters.map(filter => filter.$getSQL()) .join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `); + + if (this.count() > 1) { + return `(${sql})`; + } + + return sql; } _triggerFilterChange (filters) { diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index 3f23a70432..b497213a9a 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -37,16 +37,6 @@ class OR extends FiltersCollection { super(filters); this.JOIN_OPERATOR = 'OR'; } - - $getSQL () { - const sql = FiltersCollection.prototype.$getSQL.apply(this); - - if (this.count() > 1) { - return `(${sql})`; - } - - return sql; - } } module.exports = OR; diff --git a/src/api/v4/source/dataset.js b/src/api/v4/source/dataset.js index 4c3f7164af..634e038405 100644 --- a/src/api/v4/source/dataset.js +++ b/src/api/v4/source/dataset.js @@ -47,7 +47,7 @@ Dataset.prototype._createInternalModel = function (engine) { var internalModel = new AnalysisModel({ id: this.getId(), type: 'source', - query: `SELECT * from ${this._tableName}` + query: this._getQueryToApply() }, { camshaftReference: CamshaftReference, engine: engine @@ -57,6 +57,8 @@ Dataset.prototype._createInternalModel = function (engine) { }; Dataset.prototype._updateInternalModelQuery = function (query) { + if (!this._internalModel) return; + this._internalModel.set('query', query, { silent: true }); return this._internalModel._engine.reload() diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js index 4c1f799b40..e20447c52f 100644 --- a/src/api/v4/source/sql.js +++ b/src/api/v4/source/sql.js @@ -71,7 +71,7 @@ SQL.prototype._createInternalModel = function (engine) { var internalModel = new AnalysisModel({ id: this.getId(), type: 'source', - query: this._query + query: this._getQueryToApply() }, { camshaftReference: CamshaftReference, engine: engine From 5cd18b4acc686aa9b060d0dd1ed5351080ac892f Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 15:28:58 +0200 Subject: [PATCH 21/35] Update examples --- examples/examples.json | 20 ++++---------------- examples/public/filters/and-filter.html | 6 ++++++ examples/public/filters/category-filter.html | 4 ++++ examples/public/filters/complex-filter.html | 3 +++ examples/public/filters/range-filter.html | 4 ++++ 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/examples.json b/examples/examples.json index 6d6887558e..2230ef1537 100644 --- a/examples/examples.json +++ b/examples/examples.json @@ -132,34 +132,22 @@ { "title": "Category Filter", "desc": "Create a Category filter with checkbox selectors", - "file": { - "leaflet": "public/filters/category-filter.html", - "gmaps": "public/filters/category-filter.html" - } + "file": "public/filters/category-filter.html" }, { "title": "Range Filter", "desc": "Create a Range filter with a range slider", - "file": { - "leaflet": "public/filters/range-filter.html", - "gmaps": "public/filters/range-filter.html" - } + "file": "public/filters/range-filter.html" }, { "title": "AND Filter", "desc": "Apply an AND filter to combine multiple filters", - "file": { - "leaflet": "public/filters/and-filter.html", - "gmaps": "public/filters/and-filter.html" - } + "file": "public/filters/and-filter.html" }, { "title": "Complex Filter", "desc": "Apply several combined filters to a dataset", - "file": { - "leaflet": "public/filters/complex-filter.html", - "gmaps": "public/filters/complex-filter.html" - } + "file": "public/filters/complex-filter.html" } ] }, diff --git a/examples/public/filters/and-filter.html b/examples/public/filters/and-filter.html index 8a567df726..5922e6f00b 100644 --- a/examples/public/filters/and-filter.html +++ b/examples/public/filters/and-filter.html @@ -20,9 +20,12 @@

    AND Filter

    +

    Apply an AND filter to exclude listings that are not within the selected room types and have a higher price than the one we set.

    +
    +
    USAGE

    Change the selected price range and the selected room types to filter the listings.

    @@ -32,6 +35,7 @@

    AND Filter

    Room Types

    +
    • @@ -50,12 +54,14 @@

      Room Types

      Price

      +

      Showing listings below and equal to 40€

+
diff --git a/examples/public/filters/category-filter.html b/examples/public/filters/category-filter.html index 09fe6b40e4..d4d1e538f9 100644 --- a/examples/public/filters/category-filter.html +++ b/examples/public/filters/category-filter.html @@ -20,9 +20,12 @@

Category Filter

+

Apply a category filter to the listings shown in the visualization.

+
+
USAGE

Change the selected room types to filter the listings.

@@ -48,6 +51,7 @@

Room Types

+
diff --git a/examples/public/filters/complex-filter.html b/examples/public/filters/complex-filter.html index da776d4f48..222867eff7 100644 --- a/examples/public/filters/complex-filter.html +++ b/examples/public/filters/complex-filter.html @@ -20,6 +20,7 @@

Complex Example

+

This map has applied a filters combination that show listings meeting these conditions:

@@ -32,11 +33,13 @@

Complex Example

If you want to know more, please go to Airbnb Listings dataset.

+
USAGE

Go to the source code below, and see how filters are applied.

+
diff --git a/examples/public/filters/range-filter.html b/examples/public/filters/range-filter.html index 90846cec20..7462cd16be 100644 --- a/examples/public/filters/range-filter.html +++ b/examples/public/filters/range-filter.html @@ -20,9 +20,12 @@

Range Filter

+

Change the price filter to filter the listings shown in the visualization by price.

+
+
USAGE

Change the selected price range to filter the listings.

@@ -38,6 +41,7 @@

Price

+
From 7b1093b652eab47e29ae667950d28552ba1cc5a2 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 15:54:54 +0200 Subject: [PATCH 22/35] Get nested filter property with proper helper. --- src/api/v4/filter/base-sql.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 38112200d0..87e0badf59 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -1,5 +1,6 @@ const _ = require('underscore'); const Base = require('./base'); +const getObjectValue = require('../../../../src/util/get-object-value'); const ALLOWED_OPTIONS = ['includeNull']; const DEFAULT_JOIN_OPERATOR = 'AND'; @@ -35,7 +36,7 @@ class SQLBase extends Base { * @param {string} filterValue - The value of the filter */ set (filterType, filterValue) { - if (!filterType || !filterValue || !_.isString(filterType) || !_.isString(filterValue)) { + if (!filterType || !filterValue || !_.isString(filterType)) { return; } @@ -105,7 +106,7 @@ class SQLBase extends Base { const parameters = this.PARAMETER_SPECIFICATION[filter].parameters; const haveCorrectType = parameters.every( parameter => { - const parameterValue = _.property(parameter.name)(filters); + const parameterValue = getObjectValue(filters, parameter.name); return parameter.allowedTypes.some(type => parameterIsOfType(type, parameterValue)); } ); From cbb2604a0b2849ae723a3b00bde171e07ac9fe4d Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 15:55:16 +0200 Subject: [PATCH 23/35] Fix tests --- test/spec/api/v4/filter/base-sql.spec.js | 2 +- test/spec/api/v4/filter/filters-collection.spec.js | 3 +-- test/spec/api/v4/source/dataset.spec.js | 2 +- test/spec/api/v4/source/sql.spec.js | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/test/spec/api/v4/filter/base-sql.spec.js b/test/spec/api/v4/filter/base-sql.spec.js index e6a7240472..1c7f6baf6f 100644 --- a/test/spec/api/v4/filter/base-sql.spec.js +++ b/test/spec/api/v4/filter/base-sql.spec.js @@ -136,7 +136,7 @@ describe('api/v4/filter/base-sql', function () { }; sqlFilter.setFilters({ in: ['category 1', 'category 2'], like: '%category%' }); - expect(sqlFilter.$getSQL()).toBe("fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%'"); + expect(sqlFilter.$getSQL()).toBe("(fake_column IN ('category 1','category 2') AND fake_column LIKE '%category%')"); }); it('should call _includeNullInQuery if includeNull option is set', function () { diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js index 9440e02eb4..a7c8c5a3f0 100644 --- a/test/spec/api/v4/filter/filters-collection.spec.js +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -21,7 +21,6 @@ describe('api/v4/filter/filters-collection', function () { it('should set provided filters and call add()', function () { spyOn(FiltersCollection.prototype, 'addFilter').and.callThrough(); - debugger; const filters = [ new carto.filter.Range(column, { lt: 1 }), @@ -128,7 +127,7 @@ describe('api/v4/filter/filters-collection', function () { }); it('should build the SQL string and join filters', function () { - expect(filtersCollection.$getSQL()).toEqual("fake_column < 1 AND fake_column IN ('category')"); + expect(filtersCollection.$getSQL()).toEqual("(fake_column < 1 AND fake_column IN ('category'))"); }); }); diff --git a/test/spec/api/v4/source/dataset.spec.js b/test/spec/api/v4/source/dataset.spec.js index fb106714f9..21b68abc81 100644 --- a/test/spec/api/v4/source/dataset.spec.js +++ b/test/spec/api/v4/source/dataset.spec.js @@ -58,7 +58,7 @@ describe('api/v4/source/dataset', function () { beforeEach(function () { populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); spyOn(populatedPlacesDataset, '_updateInternalModelQuery'); - }) + }); it('should return original query if applied filters returns no SQL', function () { expect(populatedPlacesDataset._getQueryToApply()).toBe('SELECT * from ne_10m_populated_places_simple'); diff --git a/test/spec/api/v4/source/sql.spec.js b/test/spec/api/v4/source/sql.spec.js index b2c229fe7a..05d1959af6 100644 --- a/test/spec/api/v4/source/sql.spec.js +++ b/test/spec/api/v4/source/sql.spec.js @@ -148,7 +148,7 @@ describe('api/v4/source/sql', function () { beforeEach(function () { populatedPlacesSQL = new carto.source.SQL('SELECT * FROM ne_10m_populated_places_simple'); spyOn(populatedPlacesSQL, '_updateInternalModelQuery'); - }) + }); it('should return original query if applied filters returns no SQL', function () { expect(populatedPlacesSQL._getQueryToApply()).toBe('SELECT * FROM ne_10m_populated_places_simple'); From 516db89e67fe2e7da8ff33146606d377053a800a Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 15:55:24 +0200 Subject: [PATCH 24/35] Update complex filter description --- examples/public/filters/complex-filter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/public/filters/complex-filter.html b/examples/public/filters/complex-filter.html index 222867eff7..7c6a44ef53 100644 --- a/examples/public/filters/complex-filter.html +++ b/examples/public/filters/complex-filter.html @@ -27,7 +27,7 @@

Complex Example

  • Between 30€ and 40€.
  • Centro neighbourhood.
  • -
  • Entire home/apartment listings that have been reviewed after January 2015.
  • +
  • Entire home/apartment listings OR listings that have been reviewed after January 2015.

If you want to know more, please go to Airbnb Listings dataset.

From 9d72b92006932baf89774cbc3982533cf7950917 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 15:59:38 +0200 Subject: [PATCH 25/35] Avoid sending two queries at first. Update complex filter example. --- examples/public/filters/complex-filter.html | 4 ++-- src/api/v4/source/base.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/public/filters/complex-filter.html b/examples/public/filters/complex-filter.html index 7c6a44ef53..e620a08bdb 100644 --- a/examples/public/filters/complex-filter.html +++ b/examples/public/filters/complex-filter.html @@ -27,7 +27,7 @@

Complex Example

  • Between 30€ and 40€.
  • Centro neighbourhood.
  • -
  • Entire home/apartment listings OR listings that have been reviewed after January 2015.
  • +
  • Entire home/apartment listings OR listings that have been reviewed after May 2015.

If you want to know more, please go to Airbnb Listings dataset.

@@ -64,7 +64,7 @@

Complex Example

const entireHomeFilter = new carto.filter.Category('room_type', { eq: 'Entire home/apt' }); const neighbourhoodFilter = new carto.filter.Category('neighbourhood_group', { in: ['Centro'] }); const reviewsInLastYearFilter = new carto.filter.Range('last_review', { gte: new Date('2015-05-01T00:00:00.000Z') }); - const priceFilter = new carto.filter.Range('price', { gte: 30, lte: 40 }); + const priceFilter = new carto.filter.Range('price', { between: { min: 30, max: 40 } }); const filtersCombination = new carto.filter.AND([ neighbourhoodFilter, diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index e69433ea1c..4b19ba412d 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -83,8 +83,8 @@ Base.prototype.$getInternalModel = function () { }; Base.prototype.addFilter = function (filter) { - this._appliedFilters.addFilter(filter); this._hasFiltersApplied = true; + this._appliedFilters.addFilter(filter); }; Base.prototype.addFilters = function (filters) { From 68ea4b314b7922c22745ad2e6cf321a4e1374620 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 16:01:44 +0200 Subject: [PATCH 26/35] Remove commented styles in example --- examples/public/style.css | 71 --------------------------------------- 1 file changed, 71 deletions(-) diff --git a/examples/public/style.css b/examples/public/style.css index e7d9149d86..28b653f86b 100644 --- a/examples/public/style.css +++ b/examples/public/style.css @@ -394,77 +394,6 @@ aside.toolbox { } /* RANGE SLIDER */ -/* input[type="range"] { - position: relative; - display: inline-block; - width: 100%; - height: 10px; - background-color: rgb(23, 133, 251); - border-radius: 10px; - outline: none; - cursor: pointer; - margin: 0 0 22px; - -webkit-appearance: none; -} - -input[type=range]::before, -input[type=range]::after { - color: #747D82; - position: absolute; - top: 20px; -} - -input[type=range]::before { - content: attr(min-in-currency); - left: 0; -} - -input[type=range]::after { - content: attr(max-in-currency); - right: 0; -} - -input[type="range"]::-webkit-slider-runnable-track, -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; -} - -input[type="range"]::-webkit-slider-thumb, -input[type="range"]::-moz-range-thumb, -input[type="range"]::-ms-thumb { - width: 15px; - height: 15px; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 50%; - box-shadow: 0 5px 15px 0 rgba(20, 75, 102, 0.15); - margin-top: -2px; -} - -input[type="range"]::-webkit-slider-runnable-track, -input[type="range"]::-moz-range-track { - height: 10px; - background: linear-gradient(to right, transparent calc(var(--value) * 1%), rgb(226, 230, 227) 0); - border-radius: 10px; -} - - -input[type="range"]::-webkit-slider-runnable-track { - width: 100%; -} - -input[type="range"]::-ms-track { - height: 10px; - background-color: rgb(23, 133, 251); -} - -input[type="range"]::-ms-fill-lower { - background-color: rgb(226, 230, 227); -} - -input[type="range"]::-ms-fill-upper { - background-color: rgb(23, 133, 251); -} */ input[type="range"] { position: relative; From 177dc6a42854f3d78a9a164fb0a236ae6ea2a72c Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Wed, 4 Jul 2018 16:58:41 +0200 Subject: [PATCH 27/35] Bugfix --- src/api/v4/filter/base-sql.js | 2 +- src/api/v4/filter/category.js | 2 +- src/api/v4/filter/filters-collection.js | 9 ++++++--- src/api/v4/filter/range.js | 2 +- test/spec/api/v4/filter/filters-collection.spec.js | 12 ++++++------ 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/api/v4/filter/base-sql.js b/src/api/v4/filter/base-sql.js index 87e0badf59..e716687bb3 100644 --- a/src/api/v4/filter/base-sql.js +++ b/src/api/v4/filter/base-sql.js @@ -12,7 +12,7 @@ const DEFAULT_JOIN_OPERATOR = 'AND'; * * @param {string} column - The filtering will be performed against this column * @param {object} [options={}] - * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.includeNull] - Include null rows when returning data * * @class SQLBase * @extends carto.filter.Base diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index e810707121..e25f5caeab 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -31,7 +31,7 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS) * @param {string} filters.like - Return rows whose column value is like the provided value * @param {string} filters.similarTo - Return rows whose column value is similar to the provided values * @param {object} [options] - * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.includeNull] - Include null rows when returning data * * @example * // Create a filter by room type, showing only private rooms diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 6037f02b7f..81acaa2e5f 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -45,7 +45,7 @@ class FiltersCollection extends Base { if (_.contains(this._filters, filter)) return; - filter.on('change:filters', filters => this._triggerFilterChange(filters)); + this.listenTo(filter, 'change:filters', filters => this._triggerFilterChange(filters)); this._filters.push(filter); this._triggerFilterChange(); } @@ -59,9 +59,12 @@ class FiltersCollection extends Base { * @api */ removeFilter (filter) { - if (!_.contains(this._filters, filter)) return; + const filterIndex = _.indexOf(this._filters, filter); + if (filterIndex === -1) return; + + const removedElement = this._filters.splice(filterIndex, 1)[0]; + removedElement.off('change:filters', null, this); - const removedElement = this._filters.splice(_.indexOf(filter), 1); this._triggerFilterChange(); return removedElement; } diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index b21f61e08d..da366942c1 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -63,7 +63,7 @@ const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); * @param {(number|Date)} filters.notBetweenSymmetric.min - Lower value of the comparison range * @param {(number|Date)} filters.notBetweenSymmetric.max - Upper value of the comparison range * @param {object} [options] - * @param {boolean} [options.includeNull] - The operation to apply to the data + * @param {boolean} [options.includeNull] - Include null rows when returning data * * @example * // Create a filter by price, showing only listings lower than or equal to 50€, and higher than 100€ diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js index a7c8c5a3f0..d54c2806c0 100644 --- a/test/spec/api/v4/filter/filters-collection.spec.js +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -34,10 +34,11 @@ describe('api/v4/filter/filters-collection', function () { }); describe('.addFilter', function () { - let filtersCollection, rangeFilter, triggerFilterChangeSpy; + let filtersCollection, rangeFilter, triggerFilterChangeSpy, listenToChangeSpy; beforeEach(function () { triggerFilterChangeSpy = spyOn(FiltersCollection.prototype, '_triggerFilterChange'); + listenToChangeSpy = spyOn(FiltersCollection.prototype, 'listenTo'); filtersCollection = new FiltersCollection(); rangeFilter = new carto.filter.Range(column, { lt: 1 }); }); @@ -64,12 +65,11 @@ describe('api/v4/filter/filters-collection', function () { }); it('should register listener to change:filters event in the added filter', function () { - spyOn(rangeFilter, 'on'); - filtersCollection.addFilter(rangeFilter); - expect(rangeFilter.on).toHaveBeenCalled(); - expect(rangeFilter.on.calls.mostRecent().args[0]).toEqual('change:filters'); + expect(listenToChangeSpy).toHaveBeenCalled(); + expect(listenToChangeSpy.calls.mostRecent().args[0]).toBe(rangeFilter); + expect(listenToChangeSpy.calls.mostRecent().args[1]).toEqual('change:filters'); }); }); @@ -92,7 +92,7 @@ describe('api/v4/filter/filters-collection', function () { it('should remove the filter if it was already added', function () { filtersCollection.addFilter(rangeFilter); - const removedElement = filtersCollection.removeFilter(rangeFilter)[0]; + const removedElement = filtersCollection.removeFilter(rangeFilter); expect(removedElement).toBe(rangeFilter); expect(triggerFilterChangeSpy).toHaveBeenCalled(); From 22497bec4bdf3c1693f99661a82ed79c804ee554 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 5 Jul 2018 09:50:51 +0200 Subject: [PATCH 28/35] Documentation changes --- src/api/v4/filter/filters-collection.js | 6 ++--- src/api/v4/source/base.js | 32 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 81acaa2e5f..08b0a88f1f 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -32,7 +32,7 @@ class FiltersCollection extends Base { } /** - * Add a new filter to the collection + * Add a new filter to collection * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * @memberof FiltersCollection @@ -51,7 +51,7 @@ class FiltersCollection extends Base { } /** - * Remove an existing filter from the collection + * Remove an existing filter from collection * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * @returns {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} The removed element @@ -70,7 +70,7 @@ class FiltersCollection extends Base { } /** - * Get the number of added filters to the collection + * Get the number of added filters * * @returns {number} Number of contained filters * @memberof FiltersCollection diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index 4b19ba412d..a6c617ff61 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -82,20 +82,52 @@ Base.prototype.$getInternalModel = function () { return this._internalModel; }; +/** + * Add new filter to the source + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * + * @memberof carto.filter.Base + * @api + */ Base.prototype.addFilter = function (filter) { this._hasFiltersApplied = true; this._appliedFilters.addFilter(filter); }; +/** + * Add new filters to the source + * + * @param {Array} filters + * + * @memberof carto.filter.Base + * @api + */ Base.prototype.addFilters = function (filters) { filters.forEach(filter => this.addFilter(filter)); }; +/** + * Remove an existing filter from source + * + * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter + * + * @memberof carto.filter.Base + * @api + */ Base.prototype.removeFilter = function (filter) { this._appliedFilters.removeFilter(filter); this._hasFiltersApplied = Boolean(this._appliedFilters.count()); }; +/** + * Remove existing filters from source + * + * @param {Array} filters + * + * @memberof carto.filter.Base + * @api + */ Base.prototype.removeFilters = function (filters) { filters.forEach(filter => this.removeFilter(filter)); }; From 8116183ff73ea0bead51adafff798c329bd26aa3 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 5 Jul 2018 10:41:34 +0200 Subject: [PATCH 29/35] Move super call after checking that table is OK. --- src/api/v4/source/dataset.js | 4 ++-- src/api/v4/source/sql.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/v4/source/dataset.js b/src/api/v4/source/dataset.js index 634e038405..934a286c87 100644 --- a/src/api/v4/source/dataset.js +++ b/src/api/v4/source/dataset.js @@ -18,11 +18,11 @@ var CartoError = require('../error-handling/carto-error'); * @api */ function Dataset (tableName) { - Base.apply(this, arguments); - _checkTableName(tableName); this._tableName = tableName; + Base.apply(this, arguments); + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); } diff --git a/src/api/v4/source/sql.js b/src/api/v4/source/sql.js index e20447c52f..4a76941771 100644 --- a/src/api/v4/source/sql.js +++ b/src/api/v4/source/sql.js @@ -19,11 +19,11 @@ var CartoError = require('../error-handling/carto-error'); * @api */ function SQL (query) { - Base.apply(this, arguments); - _checkQuery(query); this._query = query; + Base.apply(this, arguments); + this._appliedFilters.on('change:filters', () => this._updateInternalModelQuery(this._getQueryToApply())); } From 1bed3798238eff9ce5e38367ab94aef3acb70e81 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Thu, 5 Jul 2018 13:05:29 +0200 Subject: [PATCH 30/35] Throw error if an SQL filter is passed to dataviews --- src/api/v4/dataview/base.js | 9 +++++---- test/spec/api/v4/dataview/base.spec.js | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/api/v4/dataview/base.js b/src/api/v4/dataview/base.js index a5abc1adb9..577420ecb3 100644 --- a/src/api/v4/dataview/base.js +++ b/src/api/v4/dataview/base.js @@ -3,6 +3,7 @@ var Backbone = require('backbone'); var status = require('../constants').status; var SourceBase = require('../source/base'); var FilterBase = require('../filter/base'); +var SQLFilterBase = require('../filter/base-sql'); var CartoError = require('../error-handling/carto-error'); var CartoValidationError = require('../error-handling/carto-validation-error'); @@ -11,14 +12,14 @@ var CartoValidationError = require('../error-handling/carto-validation-error'); * * Dataviews are a way to extract data from a CARTO account in predefined ways * (eg: a list of categories, the result of a formula operation, etc.). - * + * * **This object should not be used directly** * * The data used in a dataviews cames from a {@link carto.source.Base|source} that might change * due to different reasons (eg: SQL query changed). - * + * * When dataview data changes the dataview will trigger events to notify subscribers when new data is available. - * + * * @example * // Keep your widget data sync. Remember each dataview has his own data format. * dataview.on('dataChanged', newData => { @@ -246,7 +247,7 @@ Base.prototype._checkOptions = function (options) { }; Base.prototype._checkFilter = function (filter) { - if (!(filter instanceof FilterBase)) { + if (!(filter instanceof FilterBase) || filter instanceof SQLFilterBase) { throw this._getValidationError('filterRequired'); } }; diff --git a/test/spec/api/v4/dataview/base.spec.js b/test/spec/api/v4/dataview/base.spec.js index 17429099cf..1531244af3 100644 --- a/test/spec/api/v4/dataview/base.spec.js +++ b/test/spec/api/v4/dataview/base.spec.js @@ -230,5 +230,14 @@ describe('api/v4/dataview/base', function () { expect(test).toThrowError('Filter property is required.'); }); + + it('should throw an error if an SQL filter is passed', function () { + function test () { + var categoryFilter = new carto.filter.Category('fake_column', { in: ['category_value'] }); + base.addFilter(categoryFilter); + } + + expect(test).toThrowError('Filter property is required.'); + }); }); }); From 08552b376fe4515c46c5537839379e62f800b45b Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 9 Jul 2018 09:37:50 +0200 Subject: [PATCH 31/35] Documentation changes --- src/api/v4/source/base.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index a6c617ff61..d13d8284dc 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -87,7 +87,7 @@ Base.prototype.$getInternalModel = function () { * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * - * @memberof carto.filter.Base + * @memberof carto.source.Base * @api */ Base.prototype.addFilter = function (filter) { @@ -100,7 +100,7 @@ Base.prototype.addFilter = function (filter) { * * @param {Array} filters * - * @memberof carto.filter.Base + * @memberof carto.source.Base * @api */ Base.prototype.addFilters = function (filters) { @@ -112,7 +112,7 @@ Base.prototype.addFilters = function (filters) { * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter * - * @memberof carto.filter.Base + * @memberof carto.source.Base * @api */ Base.prototype.removeFilter = function (filter) { @@ -125,7 +125,7 @@ Base.prototype.removeFilter = function (filter) { * * @param {Array} filters * - * @memberof carto.filter.Base + * @memberof carto.source.Base * @api */ Base.prototype.removeFilters = function (filters) { From 4873cb57d3b8b2dc634b283dc987ebd276dd7334 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Mon, 9 Jul 2018 10:48:06 +0200 Subject: [PATCH 32/35] Remove TODO --- src/api/v4/error-handling/error-list/validation-errors.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/v4/error-handling/error-list/validation-errors.js b/src/api/v4/error-handling/error-list/validation-errors.js index 33403cfa25..dcf423d057 100644 --- a/src/api/v4/error-handling/error-list/validation-errors.js +++ b/src/api/v4/error-handling/error-list/validation-errors.js @@ -204,7 +204,6 @@ module.exports = { }, 'invalid-filter': { messageRegex: /invalidFilter(.+)/, - // TODO: Add link to documentation? friendlyMessage: "'$0' is not a valid filter. Please check documentation." }, 'invalid-option': { From c63c663c3dc57fe81975b6621304c5bd5fc7a6bc Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 10 Jul 2018 10:52:45 +0200 Subject: [PATCH 33/35] Add getFilters() --- src/api/v4/filter/filters-collection.js | 11 +++++++++++ src/api/v4/source/base.js | 11 +++++++++++ .../spec/api/v4/filter/filters-collection.spec.js | 15 ++++++++++++++- test/spec/api/v4/source/dataset.spec.js | 15 +++++++++++++++ test/spec/api/v4/source/sql.spec.js | 15 +++++++++++++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 08b0a88f1f..039733fec5 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -80,6 +80,17 @@ class FiltersCollection extends Base { return this._filters.length; } + /** + * Get added filters + * + * @returns {Array} Added filters + * @memberof FiltersCollection + * @api + */ + getFilters () { + return this._filters; + } + $getSQL () { const sql = this._filters.map(filter => filter.$getSQL()) .join(` ${this.JOIN_OPERATOR || DEFAULT_JOIN_OPERATOR} `); diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index d13d8284dc..7a85dee849 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -82,6 +82,17 @@ Base.prototype.$getInternalModel = function () { return this._internalModel; }; +/** + * Get added filters + * + * @returns {Array} Added filters + * @memberof carto.source.Base + * @api + */ +Base.prototype.getFilters = function () { + return this._appliedFilters.getFilters(); +}; + /** * Add new filter to the source * diff --git a/test/spec/api/v4/filter/filters-collection.spec.js b/test/spec/api/v4/filter/filters-collection.spec.js index d54c2806c0..3a580b5cfd 100644 --- a/test/spec/api/v4/filter/filters-collection.spec.js +++ b/test/spec/api/v4/filter/filters-collection.spec.js @@ -105,12 +105,25 @@ describe('api/v4/filter/filters-collection', function () { beforeEach(function () { filtersCollection = new FiltersCollection(); rangeFilter = new carto.filter.Range(column, { lt: 1 }); + filtersCollection.addFilter(rangeFilter); }); it('should return filters length', function () { + expect(filtersCollection.count()).toBe(1); + }); + }); + + describe('.getFilters', function () { + let filtersCollection, rangeFilter; + + beforeEach(function () { + filtersCollection = new FiltersCollection(); + rangeFilter = new carto.filter.Range(column, { lt: 1 }); filtersCollection.addFilter(rangeFilter); + }); - expect(filtersCollection.count()).toBe(1); + it('should return added filters', function () { + expect(filtersCollection.getFilters()).toEqual([rangeFilter]); }); }); diff --git a/test/spec/api/v4/source/dataset.spec.js b/test/spec/api/v4/source/dataset.spec.js index 21b68abc81..853d9ac2e9 100644 --- a/test/spec/api/v4/source/dataset.spec.js +++ b/test/spec/api/v4/source/dataset.spec.js @@ -110,6 +110,21 @@ describe('api/v4/source/dataset', function () { }); }); + describe('.getFilters', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + filter = new carto.filter.Category('fake_column', { in: ['category'] }); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + populatedPlacesDataset.addFilter(filter); + }); + + it('should return added filters', function () { + expect(populatedPlacesDataset.getFilters()).toEqual([filter]); + }); + }); + describe('errors', function () { it('should trigger an error when invalid', function (done) { var client = new carto.Client({ diff --git a/test/spec/api/v4/source/sql.spec.js b/test/spec/api/v4/source/sql.spec.js index 05d1959af6..92af2a03ac 100644 --- a/test/spec/api/v4/source/sql.spec.js +++ b/test/spec/api/v4/source/sql.spec.js @@ -200,6 +200,21 @@ describe('api/v4/source/sql', function () { }); }); + describe('.getFilters', function () { + let populatedPlacesDataset, filter; + + beforeEach(function () { + filter = new carto.filter.Category('fake_column', { in: ['category'] }); + + populatedPlacesDataset = new carto.source.Dataset('ne_10m_populated_places_simple'); + populatedPlacesDataset.addFilter(filter); + }); + + it('should return added filters', function () { + expect(populatedPlacesDataset.getFilters()).toEqual([filter]); + }); + }); + describe('errors', function () { it('should trigger an error when invalid', function (done) { var client = new carto.Client({ From 5d276ae447f9ac7b9ce168c03b2c1c894abf4a10 Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 10 Jul 2018 12:38:47 +0200 Subject: [PATCH 34/35] Remove useless documentation --- src/api/v4/filter/and.js | 4 ---- src/api/v4/filter/category.js | 4 ---- src/api/v4/filter/filters-collection.js | 2 -- src/api/v4/filter/or.js | 4 ---- src/api/v4/filter/range.js | 4 ---- 5 files changed, 18 deletions(-) diff --git a/src/api/v4/filter/and.js b/src/api/v4/filter/and.js index 60c061749e..a995030998 100644 --- a/src/api/v4/filter/and.js +++ b/src/api/v4/filter/and.js @@ -1,10 +1,6 @@ const FiltersCollection = require('./filters-collection'); /** - * AND Filter Group. - * - * SQL and Dataset source filter. - * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. * * This filter will group as many filters as you want and it will add them to the query returning the rows that match ALL the filters to render the visualization. diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index e25f5caeab..72d95b4461 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -12,10 +12,6 @@ const CATEGORY_COMPARISON_OPERATORS = { const ALLOWED_FILTERS = Object.freeze(Object.keys(CATEGORY_COMPARISON_OPERATORS)); /** - * Category Filter. - * - * SQL and Dataset source filter. - * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. * * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. diff --git a/src/api/v4/filter/filters-collection.js b/src/api/v4/filter/filters-collection.js index 039733fec5..ff82484f21 100644 --- a/src/api/v4/filter/filters-collection.js +++ b/src/api/v4/filter/filters-collection.js @@ -5,8 +5,6 @@ const SQLBase = require('./base-sql'); const DEFAULT_JOIN_OPERATOR = 'AND'; /** - * Filters Collection. - * * Base class for AND and OR filters. * * Filters Collection is a way to group a set of filters in order to create composed filters, allowing the user to change the operator that joins the filters. diff --git a/src/api/v4/filter/or.js b/src/api/v4/filter/or.js index b497213a9a..b7119890fc 100644 --- a/src/api/v4/filter/or.js +++ b/src/api/v4/filter/or.js @@ -1,10 +1,6 @@ const FiltersCollection = require('./filters-collection'); /** - * OR Filter Group. - * - * SQL and Dataset source filter. - * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within filters. * * This filter will group as many filters as you want and it will add them to the query returning the rows that match ANY of the filters to render the visualization. diff --git a/src/api/v4/filter/range.js b/src/api/v4/filter/range.js index da366942c1..386aa0096f 100644 --- a/src/api/v4/filter/range.js +++ b/src/api/v4/filter/range.js @@ -34,10 +34,6 @@ const RANGE_COMPARISON_OPERATORS = { const ALLOWED_FILTERS = Object.freeze(Object.keys(RANGE_COMPARISON_OPERATORS)); /** - * Range Filter. - * - * SQL and Dataset source filter. - * * When including this filter into a {@link carto.source.SQL} or a {@link carto.source.Dataset}, the rows will be filtered by the conditions included within the filter. * * You can filter columns with `in`, `notIn`, `eq`, `notEq`, `like`, `similarTo` filters, and update the conditions with `.set()` or `.setFilters()` method. It will refresh the visualization automatically when any filter is added or modified. From 33cd867408bf33faca11cc2fbc8d4fb923f698eb Mon Sep 17 00:00:00 2001 From: jesusbotella Date: Tue, 10 Jul 2018 12:58:13 +0200 Subject: [PATCH 35/35] Change method definition for docs --- src/api/v4/filter/category.js | 2 +- src/api/v4/source/base.js | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/api/v4/filter/category.js b/src/api/v4/filter/category.js index 72d95b4461..84fa3a9581 100644 --- a/src/api/v4/filter/category.js +++ b/src/api/v4/filter/category.js @@ -73,7 +73,7 @@ class Category extends SQLBase { /** * Set filter conditions, overriding all the previous ones. - * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. + * @param {object} filters - Object containing all the new filters to apply. Check filter options in {@link carto.filter.Category}. * * @memberof Category * @method setFilters diff --git a/src/api/v4/source/base.js b/src/api/v4/source/base.js index 7a85dee849..ab58e04d3b 100644 --- a/src/api/v4/source/base.js +++ b/src/api/v4/source/base.js @@ -86,7 +86,6 @@ Base.prototype.$getInternalModel = function () { * Get added filters * * @returns {Array} Added filters - * @memberof carto.source.Base * @api */ Base.prototype.getFilters = function () { @@ -97,8 +96,6 @@ Base.prototype.getFilters = function () { * Add new filter to the source * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter - * - * @memberof carto.source.Base * @api */ Base.prototype.addFilter = function (filter) { @@ -110,8 +107,6 @@ Base.prototype.addFilter = function (filter) { * Add new filters to the source * * @param {Array} filters - * - * @memberof carto.source.Base * @api */ Base.prototype.addFilters = function (filters) { @@ -122,8 +117,6 @@ Base.prototype.addFilters = function (filters) { * Remove an existing filter from source * * @param {(carto.filter.Range|carto.filter.Category|carto.filter.AND|carto.filter.OR)} filter - * - * @memberof carto.source.Base * @api */ Base.prototype.removeFilter = function (filter) { @@ -135,8 +128,6 @@ Base.prototype.removeFilter = function (filter) { * Remove existing filters from source * * @param {Array} filters - * - * @memberof carto.source.Base * @api */ Base.prototype.removeFilters = function (filters) {