diff --git a/NEWS.md b/NEWS.md index 2aa7f849..e3ad2917 100644 --- a/NEWS.md +++ b/NEWS.md @@ -33,6 +33,7 @@ New features: - nock: 9.2.6 - strftime: 0.10.0 - Optional instantiation metadata stats (https://github.com/CartoDB/Windshaft-cartodb/pull/952) +- Experimental dates_as_numbers support Bug Fixes: - Validates tile coordinates (z/x/y) from request params to be a valid integer value. diff --git a/lib/cartodb/api/api-router.js b/lib/cartodb/api/api-router.js index b0f42f03..6083ab0c 100644 --- a/lib/cartodb/api/api-router.js +++ b/lib/cartodb/api/api-router.js @@ -35,6 +35,7 @@ const TurboCartoAdapter = require('../models/mapconfig/adapter/turbo-carto-adapt const DataviewsWidgetsAdapter = require('../models/mapconfig/adapter/dataviews-widgets-adapter'); const AggregationMapConfigAdapter = require('../models/mapconfig/adapter/aggregation-mapconfig-adapter'); const MapConfigAdapter = require('../models/mapconfig/adapter'); +const VectorMapConfigAdapter = require('../models/mapconfig/adapter/vector-mapconfig-adapter'); const ResourceLocator = require('../models/resource-locator'); const LayergroupMetadata = require('../utils/layergroup-metadata'); @@ -135,6 +136,7 @@ module.exports = class ApiRouter { new SqlWrapMapConfigAdapter(), new DataviewsWidgetsAdapter(), new AnalysisMapConfigAdapter(analysisBackend), + new VectorMapConfigAdapter(pgConnection), new AggregationMapConfigAdapter(pgConnection), new MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend), new TurboCartoAdapter() diff --git a/lib/cartodb/api/middlewares/layergroup-metadata.js b/lib/cartodb/api/middlewares/layergroup-metadata.js index 301089df..bbb1fbe4 100644 --- a/lib/cartodb/api/middlewares/layergroup-metadata.js +++ b/lib/cartodb/api/middlewares/layergroup-metadata.js @@ -7,6 +7,7 @@ module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQu layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery); layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context); layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context); + layergroupMetadata.addDateWrappingMetadata (layergroup, mapConfig.obj()); layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig); next(); diff --git a/lib/cartodb/backends/layer-stats/mapnik-layer-stats.js b/lib/cartodb/backends/layer-stats/mapnik-layer-stats.js index d8320334..d5f841b8 100644 --- a/lib/cartodb/backends/layer-stats/mapnik-layer-stats.js +++ b/lib/cartodb/backends/layer-stats/mapnik-layer-stats.js @@ -34,12 +34,6 @@ MapnikLayerStats.prototype.is = function (type) { return this._types[type] ? this._types[type] : false; }; -function queryPromise(dbConnection, query) { - return new Promise((resolve, reject) => { - dbConnection.query(query, (err, res) => err ? reject(err) : resolve(res)); - }); - } - function columnAggregations(field) { if (field.type === 'number') { return ['min', 'max', 'avg', 'sum']; @@ -63,7 +57,7 @@ function _getSQL(ctx, query, type='pre', zoom=0) { } function _estimatedFeatureCount(ctx) { - return queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation)) + return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation)) .then(res => ({ estimatedFeatureCount: res.rows[0].rows })) .catch(() => ({ estimatedFeatureCount: -1 })); } @@ -71,7 +65,7 @@ function _estimatedFeatureCount(ctx) { function _featureCount(ctx) { if (ctx.metaOptions.featureCount) { // TODO: if ctx.metaOptions.columnStats we can combine this with column stats query - return queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount)) + return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount)) .then(res => ({ featureCount: res.rows[0].rows })); } return Promise.resolve(); @@ -82,7 +76,7 @@ function _aggrFeatureCount(ctx) { // We expect as zoom level as the value of aggrFeatureCount // TODO: it'd be nice to admit an array of zoom levels to // return metadata for multiple levels. - return queryPromise( + return queryUtils.queryPromise( ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount, 'post', ctx.metaOptions.aggrFeatureCount) ).then(res => ({ aggrFeatureCount: res.rows[0].rows })); @@ -93,7 +87,8 @@ function _aggrFeatureCount(ctx) { function _geometryType(ctx) { if (ctx.metaOptions.geometryType) { const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn(); - return queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn))) + const sqlQuery = _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn)); + return queryUtils.queryPromise(ctx.dbConnection, sqlQuery) .then(res => ({ geometryType: res.rows[0].geom_type })); } return Promise.resolve(); @@ -102,7 +97,7 @@ function _geometryType(ctx) { function _columns(ctx) { if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) { // note: post-aggregation columns are in layer.options.columns when aggregation is present - return queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0))) + return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0))) .then(res => formatResultFields(ctx.dbConnection, res.fields)); } return Promise.resolve(); @@ -156,7 +151,7 @@ function _sample(ctx, numRows) { const requestedRows = ctx.metaOptions.sample.num_rows || DEFAULT_SAMPLE_ROWS; const limit = Math.ceil(requestedRows * 1.5); let columns = ctx.metaOptions.sample.include_columns; - return queryPromise(ctx.dbConnection, _getSQL( + return queryUtils.queryPromise(ctx.dbConnection, _getSQL( ctx, sql => queryUtils.getQuerySample(sql, sampleProb, limit, SAMPLE_SEED, columns) )).then(res => ({ sample: res.rows })); @@ -186,7 +181,7 @@ function _columnStats(ctx, columns) { // TODO: ctx.metaOptions.columnStats.maxCategories // => use PG stats to dismiss columns with more distinct values queries.push( - queryPromise( + queryUtils.queryPromise( ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls)) ).then(res => ({ [name]: { categories: res.rows } })) @@ -194,7 +189,7 @@ function _columnStats(ctx, columns) { } }); queries.push( - queryPromise( + queryUtils.queryPromise( ctx.dbConnection, _getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`) ).then(res => { diff --git a/lib/cartodb/models/dataview/histograms/date-histogram.js b/lib/cartodb/models/dataview/histograms/date-histogram.js index 4c720b3b..86e0e539 100644 --- a/lib/cartodb/models/dataview/histograms/date-histogram.js +++ b/lib/cartodb/models/dataview/histograms/date-histogram.js @@ -2,6 +2,7 @@ const BaseHistogram = require('./base-histogram'); const debug = require('debug')('windshaft:dataview:date-histogram'); const utils = require('../../../utils/query-utils'); + /** * Gets the name of a timezone with the same offset as the required * using the pg_timezone_names table. We do this because it's simpler to pass diff --git a/lib/cartodb/models/mapconfig/adapter/vector-mapconfig-adapter.js b/lib/cartodb/models/mapconfig/adapter/vector-mapconfig-adapter.js new file mode 100644 index 00000000..a707b5d3 --- /dev/null +++ b/lib/cartodb/models/mapconfig/adapter/vector-mapconfig-adapter.js @@ -0,0 +1,63 @@ +const queryUtils = require('../../../utils/query-utils'); +const dateWrapper = require('../../../utils/date-wrapper'); + +/** + * This middleware wraps the layer query transforming the date fields into numbers because mvt tiles + * doesnt support dates as primitive type. + * + * - This middleware is ONLY activated when the `dates_as_numbers` option is enabled for some layer in the mapConfig. + */ +class VectorMapConfigAdapter { + constructor(pgConnection) { + this.pgConnection = pgConnection; + } + + getMapConfig(user, requestMapConfig, params, context, callback) { + if (!this._isDatesAsNumbersFlagEnabled(requestMapConfig)) { + return callback(null, requestMapConfig); + } + + this._wrapDates(requestMapConfig, user) + .then(updatedRequestMapConfig => callback(null, updatedRequestMapConfig)) + .catch(callback); + } + + _wrapDates(requestMapConfig, user) { + return Promise.all(requestMapConfig.layers.map(layer => this._wrapLayer(layer, user))) + .then(() => requestMapConfig); + } + + _wrapLayer(layer, user) { + if (!layer.options.dates_as_numbers || !layer.options.sql) { + return Promise.resolve(layer); + } + const originalQuery = layer.options.sql; + return this._getColumns(user, originalQuery) + .then(result => { + const newSqlQuery = dateWrapper.wrapDates(originalQuery, result.fields); + layer.options.sql = newSqlQuery; + return layer; + }); + } + + _getColumns(user, originalQuery) { + return new Promise((resolve, reject) => { + this.pgConnection.getConnection(user, (err, connection) => { + if (err) { + return reject(err); + } + const query = queryUtils.getQueryLimited(originalQuery, 0); + queryUtils.queryPromise(connection, query) + .then(resolve) + .catch(reject); + }); + }); + } + + _isDatesAsNumbersFlagEnabled(requestMapConfig) { + return requestMapConfig.layers && requestMapConfig.layers.some(layer => layer.options.dates_as_numbers); + } +} + + +module.exports = VectorMapConfigAdapter; \ No newline at end of file diff --git a/lib/cartodb/utils/date-wrapper.js b/lib/cartodb/utils/date-wrapper.js new file mode 100644 index 00000000..9d5657b1 --- /dev/null +++ b/lib/cartodb/utils/date-wrapper.js @@ -0,0 +1,59 @@ +const DATE_OIDS = Object.freeze({ + 1082: 'DATE', + 1083: 'TIME', + 1114: 'TIMESTAMP', + 1184: 'TIMESTAMPTZ', + 1266: 'TIMETZ' +}); + +/** + * Wrap a query transforming all date columns into a unix epoch + * @param {*} originalQuery + * @param {*} fields + */ +function wrapDates(originalQuery, fields) { + return ` + SELECT + ${fields.map(field => _isDateType(field) ? _castColumnToEpoch(field.name) : `${field.name}`).join(',')} + FROM + (${originalQuery}) _cdb_epoch_transformation `; +} + +/** + * @param {object} field + */ +function _isDateType(field) { + return DATE_OIDS.hasOwnProperty(field.dataTypeID); +} + +/** + * Return a sql query to transform a date column into a unix epoch + * @param {string} column - The name of the date column + */ +function _castColumnToEpoch(columnName) { + return `date_part('epoch', "${columnName}") as "${columnName}"`; +} + +function getColumnsWithWrappedDates(query) { + if(!query){ + return; + } + if (!query.match(/\b_cdb_epoch_transformation\b/)) { + return; + } + const columns = []; + const fieldMatcher = /\bdate_part\('epoch', "([^"]+)"\) as "([^"]+)"/gmi; + let match; + do { + match = fieldMatcher.exec(query); + if (match && match[1] === match[2]) { + columns.push(match[1]); + } + } while (match); + return columns; +} + +module.exports = { + wrapDates, + getColumnsWithWrappedDates +}; diff --git a/lib/cartodb/utils/layergroup-metadata.js b/lib/cartodb/utils/layergroup-metadata.js index 0e5d702a..41c3c9af 100644 --- a/lib/cartodb/utils/layergroup-metadata.js +++ b/lib/cartodb/utils/layergroup-metadata.js @@ -1,3 +1,5 @@ +const dateWrapper = require('./date-wrapper'); + module.exports = class LayergroupMetadata { constructor (resourceLocator) { this.resourceLocator = resourceLocator; @@ -165,4 +167,21 @@ module.exports = class LayergroupMetadata { }); } } + + addDateWrappingMetadata (layergroup, mapConfig) { + if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) { + layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) { + const mapConfigLayer = mapConfig.layers[layerIndex]; + const layerOptions = mapConfigLayer.options; + if (layerOptions.dates_as_numbers && layerOptions.sql) { + const wrappedColumns = dateWrapper.getColumnsWithWrappedDates(layerOptions.sql); + if (wrappedColumns) { + layer.meta.dates_as_numbers = wrappedColumns; + } + } + return layer; + }); + } + } + }; diff --git a/lib/cartodb/utils/query-utils.js b/lib/cartodb/utils/query-utils.js index 867db0cd..ed239422 100644 --- a/lib/cartodb/utils/query-utils.js +++ b/lib/cartodb/utils/query-utils.js @@ -1,16 +1,16 @@ function prepareQuery(sql) { - var affectedTableRegexCache = { - bbox: /!bbox!/g, - scale_denominator: /!scale_denominator!/g, - pixel_width: /!pixel_width!/g, - pixel_height: /!pixel_height!/g - }; + var affectedTableRegexCache = { + bbox: /!bbox!/g, + scale_denominator: /!scale_denominator!/g, + pixel_width: /!pixel_width!/g, + pixel_height: /!pixel_height!/g + }; - return sql - .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') - .replace(affectedTableRegexCache.scale_denominator, '0') - .replace(affectedTableRegexCache.pixel_width, '1') - .replace(affectedTableRegexCache.pixel_height, '1'); + return sql + .replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)') + .replace(affectedTableRegexCache.scale_denominator, '0') + .replace(affectedTableRegexCache.pixel_width, '1') + .replace(affectedTableRegexCache.pixel_height, '1'); } module.exports.extractTableNames = function extractTableNames(query) { @@ -59,25 +59,25 @@ module.exports.handleFloatColumn = function handleFloatColumn(ctx) { }; /** Count NULL appearances */ -module.exports.countNULLs= function countNULLs(ctx) { +module.exports.countNULLs = function countNULLs(ctx) { return `sum(CASE WHEN (${ctx.column} IS NULL) THEN 1 ELSE 0 END)`; }; /** Count only infinity (positive and negative) appearances */ module.exports.countInfinites = function countInfinites(ctx) { - return `${!ctx.isFloatColumn ? `0` : + return `${!ctx.isFloatColumn ? '0' : `sum(CASE WHEN (${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float) THEN 1 ELSE 0 END)` }`; }; /** Count only NaNs appearances*/ module.exports.countNaNs = function countNaNs(ctx) { - return `${!ctx.isFloatColumn ? `0` : + return `${!ctx.isFloatColumn ? '0' : `sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)` }`; }; -module.exports.getQueryTopCategories = function(query, column, topN, includeNulls=false) { +module.exports.getQueryTopCategories = function (query, column, topN, includeNulls = false) { const where = includeNulls ? '' : `WHERE ${column} IS NOT NULL`; return ` SELECT ${column} AS category, COUNT(*) AS frequency @@ -101,7 +101,7 @@ function columnSelector(columns) { throw new TypeError(`Bad argument type for columns: ${typeof columns}`); } -module.exports.getQuerySample = function(query, sampleProb, limit = null, randomSeed = 0.5, columns = null) { +module.exports.getQuerySample = function (query, sampleProb, limit = null, randomSeed = 0.5, columns = null) { const singleTable = simpleQueryTable(query); if (singleTable) { return getTableSample(singleTable.table, columns || singleTable.columns, sampleProb, limit, randomSeed); @@ -121,7 +121,7 @@ module.exports.getQuerySample = function(query, sampleProb, limit = null, random function getTableSample(table, columns, sampleProb, limit = null, randomSeed = 0.5) { const limitClause = limit ? `LIMIT ${limit}` : ''; sampleProb *= 100; - randomSeed *= Math.pow(2, 31) -1; + randomSeed *= Math.pow(2, 31) - 1; return ` SELECT ${columnSelector(columns)} FROM ${table} @@ -132,12 +132,12 @@ function getTableSample(table, columns, sampleProb, limit = null, randomSeed = 0 function simpleQueryTable(sql) { const basicQuery = /\s*SELECT\s+([\*a-z0-9_,\s]+?)\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i; - const unwrappedQuery = new RegExp("^"+basicQuery.source+"$", 'i'); + const unwrappedQuery = new RegExp('^' + basicQuery.source + '$', 'i'); // queries for named maps are wrapped like this: var wrappedQuery = new RegExp( - "^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(" + + '^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(' + basicQuery.source + - "\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$", + '\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$', 'i' ); let match = sql.match(unwrappedQuery); @@ -147,13 +147,13 @@ function simpleQueryTable(sql) { if (match) { const columns = match[1]; const schema = match[3]; - const table = match[4]; + const table = match[4]; return { table: schema ? `${schema}.${table}` : table, columns }; } return false; } -module.exports.getQueryGeometryType = function(query, geometryColumn) { +module.exports.getQueryGeometryType = function (query, geometryColumn) { return ` SELECT ST_GeometryType(${geometryColumn}) AS geom_type FROM (${query}) AS __cdb_query @@ -162,10 +162,19 @@ module.exports.getQueryGeometryType = function(query, geometryColumn) { `; }; -module.exports.getQueryLimited = function(query, limit=0) { +function getQueryLimited(query, limit = 0) { return ` SELECT * FROM (${query}) AS __cdb_query LIMIT ${limit} `; -}; +} + +function queryPromise(dbConnection, query) { + return new Promise((resolve, reject) => { + dbConnection.query(query, (err, res) => err ? reject(err) : resolve(res)); + }); +} + +module.exports.queryPromise = queryPromise; +module.exports.getQueryLimited = getQueryLimited; \ No newline at end of file diff --git a/test/acceptance/date-wrapping.spec.js b/test/acceptance/date-wrapping.spec.js new file mode 100644 index 00000000..9bb8098f --- /dev/null +++ b/test/acceptance/date-wrapping.spec.js @@ -0,0 +1,265 @@ +/* eslint-env mocha */ +const assert = require('assert'); +const TestClient = require('../support/test-client'); +const mapConfigFactory = require('../fixtures/test_mapconfigFactory'); + +describe('date-wrapping', () => { + let testClient; + + describe('when a map instantiation has one single layer', () => { + describe('and the layer has the "dates_as_numbers" option enabled', () => { + beforeEach(() => { + const mapConfig = mapConfigFactory.getVectorMapConfig({ layerOptions: [{ dates_as_numbers: true }]}); + testClient = new TestClient(mapConfig); + }); + + afterEach(done => testClient.drain(done)); + + it('should return date columns casted as numbers', done => { + + testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => { + const expected = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 } + } + ]; + const actual = JSON.parse(mvt.toGeoJSONSync(0)).features; + + assert.deepEqual(actual, expected); + done(); + }); + }); + + it('should return metadata with casted columns', done => { + + testClient.getLayergroup(function(err, layergroup) { + assert.ifError(err); + assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers, ['date']); + done(); + }); + + }); + + }); + + describe('and the layer has the "dates_as_numbers" option disabled', () => { + beforeEach(() => { + const mapConfig = mapConfigFactory.getVectorMapConfig({ layerOptions: [{ dates_as_numbers: false }]}); + testClient = new TestClient(mapConfig); + }); + + afterEach(done => testClient.drain(done)); + + it('should return date columns as dates', done => { + + testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => { + const expected = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1 } + } + ]; + const actual = JSON.parse(mvt.toGeoJSONSync(0)).features; + + assert.deepEqual(actual, expected); + done(); + }); + }); + }); + }); + + describe('when a map instantiation has multiple layers', () => { + afterEach(done => testClient.drain(done)); + + describe('and both layers have the "dates_as_numbers" option enabled', () => { + beforeEach(() => { + const mapConfig = mapConfigFactory.getVectorMapConfig({ + numberOfLayers: 2, + layerOptions: [ + { dates_as_numbers: true }, + { dates_as_numbers: true } + ] + }); + testClient = new TestClient(mapConfig); + }); + + it('should return dates as numbers for every layer', done => { + testClient.getLayergroup(function(err, layergroup) { + assert.ifError(err); + assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers, ['date']); + assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers, ['date']); + }); + + testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => { + const expected0 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 } + } + ]; + const expected1 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 } + } + ]; + const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features; + const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features; + + assert.deepEqual(actual0, expected0); + assert.deepEqual(actual1, expected1); + done(); + }); + }); + }); + + describe('and only one layers has the "dates_as_numbers" option enabled', () => { + beforeEach(() => { + const mapConfig = mapConfigFactory.getVectorMapConfig({ + numberOfLayers: 2, + layerOptions: [ + { dates_as_numbers: false }, + { dates_as_numbers: true } + ] + }); + testClient = new TestClient(mapConfig); + }); + + it('should return dates as numbers only for the layer with the "dates_as_numbers" flag enabled', done => { + testClient.getLayergroup(function(err, layergroup) { + assert.ifError(err); + assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers || [], []); + assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers, ['date']); + }); + + testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => { + const expected0 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1 } + } + ]; + const expected1 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0, date: 1527810000 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1, date: 1527900000 } + } + ]; + const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features; + const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features; + + assert.deepEqual(actual0, expected0); + assert.deepEqual(actual1, expected1); + done(); + }); + }); + }); + + describe('and none of the layers has the "dates_as_numbers" option enabled', () => { + beforeEach(() => { + const mapConfig = mapConfigFactory.getVectorMapConfig({ + numberOfLayers: 2, + layerOptions: [ + { dates_as_numbers: false }, + { dates_as_numbers: false } + ] + }); + testClient = new TestClient(mapConfig); + }); + + it('should return dates as dates for both layers', done => { + testClient.getLayergroup(function(err, layergroup) { + assert.ifError(err); + assert.deepEqual(layergroup.metadata.layers[0].meta.dates_as_numbers || [], []); + assert.deepEqual(layergroup.metadata.layers[1].meta.dates_as_numbers || [], []); + }); + + testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, mvt) => { + const expected0 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1 } + } + ]; + const expected1 = [ + { + type: 'Feature', + id: 1, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 0 } + }, + { + type: 'Feature', + id: 2, + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: { _cdb_feature_count: 1, cartodb_id: 1 } + } + ]; + const actual0 = JSON.parse(mvt.toGeoJSONSync(0)).features; + const actual1 = JSON.parse(mvt.toGeoJSONSync(1)).features; + + assert.deepEqual(actual0, expected0); + assert.deepEqual(actual1, expected1); + done(); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/fixtures/test_mapconfigFactory.js b/test/fixtures/test_mapconfigFactory.js new file mode 100644 index 00000000..ff554be5 --- /dev/null +++ b/test/fixtures/test_mapconfigFactory.js @@ -0,0 +1,62 @@ +function getVectorMapConfig(opts) { + return { + buffersize: { + mvt: 1 + }, + layers: _generateLayers(opts), + }; +} + +function _generateLayers(opts) { + const numberOfLayers = opts.numberOfLayers || 1; + const layers = []; + for (let index = 0; index < numberOfLayers; index++) { + const layerOptions = (opts.layerOptions || {})[index] || {}; + layers.push(_generateLayerConfig(layerOptions)); + } + return layers; +} + +function _generateLayerConfig(opts) { + return { + type: 'mapnik', + options: { + sql: ` + SELECT + (DATE '2018-06-01' + x) as date, + x as cartodb_id, + st_makepoint(x * 10, x * 10) as the_geom, + st_makepoint(x * 10, x * 10) as the_geom_webmercator + FROM + generate_series(0, 1) x`, + aggregation: { + columns: {}, + dimensions: { + date: 'date' + }, + placement: 'centroid', + resolution: 1, + threshold: 1 + }, + dates_as_numbers: opts.dates_as_numbers, + metadata: { + geometryType: true, + columnStats: { + topCategories: 32768, + includeNulls: true + }, + sample: { + num_rows: 1000, + include_columns: [ + 'date' + ] + } + } + } + }; +} + + + + +module.exports = { getVectorMapConfig }; \ No newline at end of file