diff --git a/mod/query.js b/mod/query.js index 7fbcb7809..e2451d29f 100644 --- a/mod/query.js +++ b/mod/query.js @@ -82,7 +82,7 @@ module.exports = async function query(req, res) { req.params.SQL = []; // Assign role filter and viewport params from layer object. - await layerQuery(req, res) + await layerQuery(req, res, template) if (res.finished) return; @@ -106,6 +106,8 @@ module.exports = async function query(req, res) { @description Queries which reference a layer must be checked against the layer JSON in the workspace. +Layer query templates must have a layer request property. + Layer queries have restricted viewport and filter params. These params can not be substituted in the database but must be replaced in the SQL query string. Any query which references a layer and locale will be passed through the layer query method. The getLayer method will fail return an error if the locale is not defined as param or the layer is not a member of the locale. @@ -116,16 +118,24 @@ Any query which references a layer and locale will be passed through the layer q @param {req} req HTTP request. @param {res} res HTTP response. +@param {Object} template The query template. +@property {Boolean} template.layer A layer query template. @property {Object} req.params Request params. @property {Object} params.filter JSON filter which must be turned into a SQL filter string for substitution. @property {Array} params.SQL Substitute parameter for SQL query. @property {Object} [params.user] Requesting user. @property {Array} [user.roles] User roles. */ -async function layerQuery(req, res) { +async function layerQuery(req, res, template) { if (!req.params.layer) { + if (template.layer) { + + // Layer query templates must have a layer request property. + return res.status(400).send(`${req.params.template} query requires a valid layer request parameter.`) + } + // Reserved params will be deleted to prevent DDL injection. delete req.params.filter delete req.params.viewport diff --git a/mod/workspace/getLayer.js b/mod/workspace/getLayer.js index 4c56f3de1..5ff375ad6 100644 --- a/mod/workspace/getLayer.js +++ b/mod/workspace/getLayer.js @@ -5,6 +5,7 @@ The getLayer module exports the getLayer method which is required by the query a @requires /utils/roles @requires /workspace/mergeTemplates @requires /workspace/getLocale +@requires /workspace/getTemplate @module /workspace/getLayer */ @@ -15,6 +16,8 @@ const mergeTemplates = require('./mergeTemplates') const getLocale = require('./getLocale') +const getTemplate = require('./getTemplate') + /** @function getLayer @async @@ -22,6 +25,8 @@ const getLocale = require('./getLocale') @description The layer locale is requested from the getLocale module. +A layer template lookup will be attempted if a layer is not found in locale.layers. + The mergeTemplate module will be called to merge templates into the locale object and substitute SRC_* environment variables. A role check is performed to check whether the requesting user has access to the locale. @@ -45,11 +50,22 @@ module.exports = async function getLayer(params) { // getLocale will return err if role.check fails. if (locale instanceof Error) return locale + let layer; + if (!Object.hasOwn(locale.layers, params.layer)) { - return new Error('Unable to validate layer param.') - } - let layer = locale.layers[params.layer] + // A layer maybe defined as a template only. + layer = await getTemplate(params.layer) + + if (!layer || layer instanceof Error) { + + return new Error('Unable to validate layer param.') + } + + } else { + + layer = locale.layers[params.layer] + } // layer maybe null. if (!layer) return; diff --git a/mod/workspace/templates/_queries.js b/mod/workspace/templates/_queries.js index dccb060d3..6e0359e9b 100644 --- a/mod/workspace/templates/_queries.js +++ b/mod/workspace/templates/_queries.js @@ -7,6 +7,7 @@ module.exports = { template: require('./gaz_query'), }, get_last_location: { + layer: true, render: require('./get_last_location'), }, distinct_values: { @@ -31,24 +32,30 @@ module.exports = { render: require('./get_nnearest'), }, geojson: { + layer: true, render: require('./geojson'), }, cluster: { + layer: true, render: require('./cluster'), reduce: true }, cluster_hex: { + layer: true, render: require('./cluster_hex'), reduce: true }, wkt: { + layer: true, render: require('./wkt'), reduce: true }, infotip: { + layer: true, render: require('./infotip'), }, layer_extent: { + layer: true, template: require('./layer_extent'), }, st_intersects_ab: { @@ -64,30 +71,38 @@ module.exports = { template: require('./st_distance_ab_multiple'), }, location_get: { + layer: true, render: require('./location_get'), }, location_new: { + layer: true, render: require('./location_new'), value_only: true }, location_delete: { + layer: true, render: require('./location_delete'), }, locations_delete: { + layer: true, render: require('./locations_delete'), }, location_update: { + layer: true, render: require('./location_update'), }, location_count: { + layer: true, template: require('./location_count'), value_only: true }, mvt: { + layer: true, render: require('./mvt'), value_only: true }, mvt_geom: { + layer: true, render: require('./mvt_geom'), value_only: true } diff --git a/mod/workspace/templates/cluster.js b/mod/workspace/templates/cluster.js index c78682eca..850abebbf 100644 --- a/mod/workspace/templates/cluster.js +++ b/mod/workspace/templates/cluster.js @@ -1,23 +1,28 @@ -module.exports = _ => { +/** +### /workspace/templates/cluster + +The cluster layer query template returns aggregated cluster features. - _.qID ??= _.layer.qID || null - _.geom ??= _.layer.geom +@module /workspace/templates/cluster +*/ +module.exports = _ => { - // Get fields array from query params. - const fields = _.fields?.split(',') - .map(field => `${_.workspace.templates[field]?.template || field} as ${field}`) + _.qID ??= _.layer.qID || null + _.geom ??= _.layer.geom - const aggFields = _.fields?.split(',') - .map(field => `CASE WHEN count(*)::int = 1 THEN (array_agg(${field}))[1] END as ${field}`) + // Get fields array from query params. + const fields = _.fields?.split(',') + .map(field => `${_.workspace.templates[field]?.template || field} as ${field}`) - const where = _.viewport || `AND ${_.geom} IS NOT NULL` + const aggFields = _.fields?.split(',') + .map(field => `CASE WHEN count(*)::int = 1 THEN (array_agg(${field}))[1] END as ${field}`) - // Calculate grid resolution (r) based on zoom level and resolution parameter. - const r = parseInt(40075016.68 / Math.pow(2, _.z) * _.resolution); + const where = _.viewport || `AND ${_.geom} IS NOT NULL` - // ${params.cat && `${params.aggregate || 'array_agg'}(cat) cat,` || ''} + // Calculate grid resolution (r) based on zoom level and resolution parameter. + const r = parseInt(40075016.68 / Math.pow(2, _.z) * _.resolution); - return ` + return ` SELECT ARRAY[x_round, y_round], count(*)::int, @@ -38,5 +43,4 @@ module.exports = _ => { WHERE TRUE ${where} \${filter}) grid GROUP BY x_round, y_round;` - -} \ No newline at end of file +} diff --git a/mod/workspace/templates/cluster_hex.js b/mod/workspace/templates/cluster_hex.js index eb8985307..c02980a1f 100644 --- a/mod/workspace/templates/cluster_hex.js +++ b/mod/workspace/templates/cluster_hex.js @@ -1,102 +1,105 @@ -module.exports = _ => { +/** +### /workspace/templates/cluster_hex - _.qID ??= _.layer.qID || null - _.geom ??= _.layer.geom +The cluster_hex layer query template returns aggregated cluster features. - // Get fields array from query params. - const fields = _.fields?.split(',') - .map(field => `${_.workspace.templates[field]?.template || field} as ${field}`) +@module /workspace/templates/cluster_hex +*/ +module.exports = _ => { - const aggFields = _.fields?.split(',') - .map(field => `CASE WHEN count(*)::int = 1 THEN (array_agg(${field}))[1] END as ${field}`) + _.qID ??= _.layer.qID || null + _.geom ??= _.layer.geom - const where = _.viewport || `AND ${_.geom} IS NOT NULL` + // Get fields array from query params. + const fields = _.fields?.split(',') + .map(field => `${_.workspace.templates[field]?.template || field} as ${field}`) - // Calculate grid resolution (r) based on zoom level and resolution parameter. - const r = parseInt(40075016.68 / Math.pow(2, _.z) * _.resolution); + const aggFields = _.fields?.split(',') + .map(field => `CASE WHEN count(*)::int = 1 THEN (array_agg(${field}))[1] END as ${field}`) - // ${params.cat && `${params.aggregate || 'array_agg'}(cat) cat,` || ''} + const where = _.viewport || `AND ${_.geom} IS NOT NULL` - const _width = r; - const _height = r - ((r * 2 / Math.sqrt(3)) - r) / 2; + // Calculate grid resolution (r) based on zoom level and resolution parameter. + const r = parseInt(40075016.68 / Math.pow(2, _.z) * _.resolution); - return ` + const _width = r; + const _height = r - ((r * 2 / Math.sqrt(3)) - r) / 2; - WITH first as ( + return ` + WITH first as ( SELECT - ${_.qID} AS id, - ${_.fields ? fields.join() + ',' : ''} - ${_.geom} AS geom, - ST_X(${_.geom}) AS x, - ST_Y(${_.geom}) AS y, - - ((ST_Y(${_.geom}) / ${_height})::integer % 2) odds, - - CASE WHEN ((ST_Y(${_.geom}) / ${_height})::integer % 2) = 0 THEN - ST_Point( - round(ST_X(${_.geom}) / ${_width}) * ${_width}, - round(ST_Y(${_.geom}) / ${_height}) * ${_height}) + ${_.qID} AS id, + ${_.fields ? fields.join() + ',' : ''} + ${_.geom} AS geom, + ST_X(${_.geom}) AS x, + ST_Y(${_.geom}) AS y, + + ((ST_Y(${_.geom}) / ${_height})::integer % 2) odds, + + CASE WHEN ((ST_Y(${_.geom}) / ${_height})::integer % 2) = 0 THEN + ST_Point( + round(ST_X(${_.geom}) / ${_width}) * ${_width}, + round(ST_Y(${_.geom}) / ${_height}) * ${_height}) - ELSE ST_Point( - round(ST_X(${_.geom}) / ${_width}) * ${_width} + ${_width / 2}, - round(ST_Y(${_.geom}) / ${_height}) * ${_height}) + ELSE ST_Point( + round(ST_X(${_.geom}) / ${_width}) * ${_width} + ${_width / 2}, + round(ST_Y(${_.geom}) / ${_height}) * ${_height}) - END p0 + END p0 FROM ${_.table} WHERE TRUE ${where} \${filter}) SELECT - ARRAY[ST_X(point), ST_Y(point)], - count(*)::int, - CASE - WHEN count(*)::int = 1 THEN (array_agg(id))[1]::varchar - ELSE CONCAT('!',(array_agg(id))[1]::varchar) - END AS id + ARRAY[ST_X(point), ST_Y(point)], + count(*)::int, + CASE + WHEN count(*)::int = 1 THEN (array_agg(id))[1]::varchar + ELSE CONCAT('!',(array_agg(id))[1]::varchar) + END AS id - ${_.fields ? ',' + aggFields.join() : ''} + ${_.fields ? ',' + aggFields.join() : ''} FROM ( SELECT - ${_.qID} as id, - ${_.fields ? fields.join() + ',' : ''} + ${_.qID} as id, + ${_.fields ? fields.join() + ',' : ''} - CASE WHEN odds = 0 THEN CASE + CASE WHEN odds = 0 THEN CASE - WHEN x < ST_X(p0) THEN CASE + WHEN x < ST_X(p0) THEN CASE - WHEN y < ST_Y(p0) THEN CASE - WHEN (geom <#> ST_Translate(p0, -${_width / 2}, -${_height})) < (geom <#> p0) - THEN ST_SnapToGrid(ST_Translate(p0, -${_width / 2}, -${_height}), 1) + WHEN y < ST_Y(p0) THEN CASE + WHEN (geom <#> ST_Translate(p0, -${_width / 2}, -${_height})) < (geom <#> p0) + THEN ST_SnapToGrid(ST_Translate(p0, -${_width / 2}, -${_height}), 1) ELSE ST_SnapToGrid(p0, 1) END - ELSE CASE - WHEN (geom <#> ST_Translate(p0, -${_width / 2}, ${_height})) < (geom <#> p0) - THEN ST_SnapToGrid(ST_Translate(p0, -${_width / 2}, ${_height}), 1) + ELSE CASE + WHEN (geom <#> ST_Translate(p0, -${_width / 2}, ${_height})) < (geom <#> p0) + THEN ST_SnapToGrid(ST_Translate(p0, -${_width / 2}, ${_height}), 1) ELSE ST_SnapToGrid(p0, 1) END END ELSE CASE - WHEN y < ST_Y(p0) THEN CASE WHEN (geom <#> ST_Translate(p0, ${_width / 2}, -${_height})) < (geom <#> p0) THEN ST_SnapToGrid(ST_Translate(p0, ${_width / 2}, -${_height}), 1) - ELSE ST_SnapToGrid(p0, 1) - END + ELSE ST_SnapToGrid(p0, 1) + END ELSE CASE WHEN (geom <#> ST_Translate(p0, ${_width / 2}, ${_height})) < (geom <#> p0) THEN ST_SnapToGrid(ST_Translate(p0, ${_width / 2}, ${_height}), 1) - ELSE ST_SnapToGrid(p0, 1) - END + ELSE ST_SnapToGrid(p0, 1) + END - END + END - END + END ELSE CASE WHEN x < (ST_X(p0) - ${_width / 2}) THEN CASE @@ -132,9 +135,7 @@ module.exports = _ => { END END as point - FROM first - ) AS grid - - GROUP BY point;` + FROM first) AS grid -} \ No newline at end of file + GROUP BY point;` +} diff --git a/mod/workspace/templates/geojson.js b/mod/workspace/templates/geojson.js index d9a9509fa..2508872d4 100644 --- a/mod/workspace/templates/geojson.js +++ b/mod/workspace/templates/geojson.js @@ -1,23 +1,30 @@ +/** +### /workspace/templates/geojson + +The geojson layer query template returns an array of records including a geojson geometry. + +@module /workspace/templates/geojson +*/ module.exports = _ => { - let properties = ''; + let properties = ''; - if (_.fields) { - const propertyKeyValuePairs = _.fields?.split(',').map(field => { - const value = _.workspace.templates[field]?.template || field; - return `'${field}',${value}`; - }); - properties = ', json_build_object(' + propertyKeyValuePairs.join(', ') + ') as properties'; - } + if (_.fields) { + const propertyKeyValuePairs = _.fields?.split(',').map(field => { + const value = _.workspace.templates[field]?.template || field; + return `'${field}',${value}`; + }); + properties = ', json_build_object(' + propertyKeyValuePairs.join(', ') + ') as properties'; + } - const where = _.viewport || `AND ${_.geom || _.layer.geom} IS NOT NULL` + const where = _.viewport || `AND ${_.geom || _.layer.geom} IS NOT NULL` - return ` - SELECT - 'Feature' AS type, - \${qID} AS id, - ST_asGeoJson(${_.geom || _.layer.geom})::json AS geometry - ${properties} - FROM \${table} - WHERE TRUE ${where} \${filter};` -} \ No newline at end of file + return ` + SELECT + 'Feature' AS type, + \${qID} AS id, + ST_asGeoJson(${_.geom || _.layer.geom})::json AS geometry + ${properties} + FROM \${table} + WHERE TRUE ${where} \${filter};` +} diff --git a/mod/workspace/templates/get_last_location.js b/mod/workspace/templates/get_last_location.js index 9b2ba3781..66de9ff6f 100644 --- a/mod/workspace/templates/get_last_location.js +++ b/mod/workspace/templates/get_last_location.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/get_last_location + +The get_last_location layer query template returns the last id from layer table in a descending order. + +@module /workspace/templates/get_last_location +*/ module.exports = _ => { const table = _.layer.table || Object.values(_.layer.tables).find(tab => !!tab); @@ -5,10 +12,10 @@ module.exports = _ => { const geom = _.layer.geom || Object.values(_.layer.geoms).find(tab => !!tab); return ` - SELECT - ${_.layer.qID} as id - FROM ${table} - WHERE ${geom} IS NOT NULL AND ${_.layer.qID} IS NOT NULL \${filter} - ORDER BY ${_.layer.qID} DESC - LIMIT 1` -} \ No newline at end of file + SELECT + ${_.layer.qID} as id + FROM ${table} + WHERE ${geom} IS NOT NULL AND ${_.layer.qID} IS NOT NULL \${filter} + ORDER BY ${_.layer.qID} DESC + LIMIT 1` +} diff --git a/mod/workspace/templates/get_nnearest.js b/mod/workspace/templates/get_nnearest.js index 01def2ef0..c651b617c 100644 --- a/mod/workspace/templates/get_nnearest.js +++ b/mod/workspace/templates/get_nnearest.js @@ -1,9 +1,8 @@ module.exports = _ => ` - SELECT \${qID} AS ID, \${label} AS label, array[st_x(st_centroid(\${geom})), st_y(st_centroid(\${geom}))] AS coords FROM \${table} WHERE true \${filter} - ORDER BY ST_Point(%{x},%{y}) <#> \${geom} LIMIT ${parseInt(_.n) || 99};` \ No newline at end of file + ORDER BY ST_Point(%{x},%{y}) <#> \${geom} LIMIT ${parseInt(_.n) || 99};` diff --git a/mod/workspace/templates/infotip.js b/mod/workspace/templates/infotip.js index 2f8b4262a..c63c160af 100644 --- a/mod/workspace/templates/infotip.js +++ b/mod/workspace/templates/infotip.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/infotip + +The infotip layer query returns a field property value from the location nearest to the provided coordinate. + +@module /workspace/templates/infotip +*/ module.exports = _ => { if (!_.coords) return ` @@ -12,4 +19,4 @@ module.exports = _ => { FROM \${table} WHERE true \${filter} ORDER BY ST_Point(${coords[0]},${coords[1]}) <#> \${geom} LIMIT 1` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/layer_extent.js b/mod/workspace/templates/layer_extent.js index 0546d2380..9c3f49c2e 100644 --- a/mod/workspace/templates/layer_extent.js +++ b/mod/workspace/templates/layer_extent.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/layer_extent + +The layer_extent layer query returns the bbox coordinates of feature [record] geometries which which pass the provided layer filter. + +@module /workspace/templates/layer_extent +*/ module.exports = ` SELECT Box2D( @@ -7,4 +14,4 @@ module.exports = ` \${proj}), \${srid})) FROM \${table} - WHERE true \${filter};` \ No newline at end of file + WHERE true \${filter};` diff --git a/mod/workspace/templates/location_count.js b/mod/workspace/templates/location_count.js index f00c77395..230b4395d 100644 --- a/mod/workspace/templates/location_count.js +++ b/mod/workspace/templates/location_count.js @@ -1,4 +1,11 @@ +/** +### /workspace/templates/location_count + +The location_count layer query returns the count of table records which pass the provided layer filter and viewport. + +@module /workspace/templates/location_count +*/ module.exports = ` - SELECT count(*) as location_count - FROM \${table} - WHERE true \${filter} \${viewport}` + SELECT count(*) as location_count + FROM \${table} + WHERE true \${filter} \${viewport}` diff --git a/mod/workspace/templates/location_delete.js b/mod/workspace/templates/location_delete.js index 9a8ec3d67..2b5be5849 100644 --- a/mod/workspace/templates/location_delete.js +++ b/mod/workspace/templates/location_delete.js @@ -1,4 +1,11 @@ +/** +### /workspace/templates/location_delete + +The location_delete layer query removes a record from a layer table where the layer qID matches the provided id property. + +@module /workspace/templates/location_delete +*/ module.exports = _ => { return `DELETE FROM ${_.table} WHERE ${_.layer.qID} = %{id};` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/location_get.js b/mod/workspace/templates/location_get.js index 431acf72d..9b7091d25 100644 --- a/mod/workspace/templates/location_get.js +++ b/mod/workspace/templates/location_get.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/location_get + +The location_get layer query returns the field values from a location record in the layer table where the location qID matches the provided id param. + +@module /workspace/templates/location_get +*/ module.exports = _ => { // The SQL array may be populated by a default filter which is not required for this query template. @@ -18,4 +25,4 @@ module.exports = _ => { SELECT ${fields.join()} FROM ${_.table} WHERE ${_.layer.qID} = %{id}` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/location_new.js b/mod/workspace/templates/location_new.js index 3e5c8606c..4bdd482d1 100644 --- a/mod/workspace/templates/location_new.js +++ b/mod/workspace/templates/location_new.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/location_new + +The location_new layer query returns the serial id for a new location record inserted into the table property. + +@module /workspace/templates/location_new +*/ module.exports = _ => { // select array for insert statement @@ -36,4 +43,4 @@ module.exports = _ => { INSERT INTO ${_.table} (${fields.join(',')}) SELECT ${selects.join(',')} RETURNING ${_.layer.qID}::varchar AS id;` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/location_update.js b/mod/workspace/templates/location_update.js index 5b56f4a1b..67a031433 100644 --- a/mod/workspace/templates/location_update.js +++ b/mod/workspace/templates/location_update.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/location_update + +The location_update layer query updates a record in the layer table identified where layer qID matches the provided id property. + +@module /workspace/templates/location_update +*/ module.exports = _ => { // The location ID must not be altered. @@ -69,4 +76,4 @@ module.exports = _ => { UPDATE ${_.table} SET ${fields.join()} WHERE ${_.layer.qID} = %{id};` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/locations_delete.js b/mod/workspace/templates/locations_delete.js index d5700d97c..a2f19fc26 100644 --- a/mod/workspace/templates/locations_delete.js +++ b/mod/workspace/templates/locations_delete.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/locations_delete + +The locations_delete layer query deletes multiple records in a layer table which pass the provided viewport and/or SQL filter. + +@module /workspace/templates/locations_delete +*/ module.exports = _ => { // If no layer parameter, return @@ -8,4 +15,4 @@ module.exports = _ => { return ` DELETE FROM ${_.table || _.layer.table} WHERE TRUE ${_.viewport || ''} \${filter};` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/mvt.js b/mod/workspace/templates/mvt.js index cc116819c..6d5f8dc1d 100644 --- a/mod/workspace/templates/mvt.js +++ b/mod/workspace/templates/mvt.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/mvt + +The mvt layer query template returns a vector tile (st_asmvt) with mvt geometries and their associated field properties. + +@module /workspace/templates/mvt +*/ module.exports = _ => { // Get fields array from query params. @@ -37,4 +44,4 @@ module.exports = _ => { ) \${filter} ) tile` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/mvt_geom.js b/mod/workspace/templates/mvt_geom.js index 0467aa393..bf81a9ead 100644 --- a/mod/workspace/templates/mvt_geom.js +++ b/mod/workspace/templates/mvt_geom.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/mvt_geom + +The mvt layer query template returns a vector tile (st_asmvt) with mvt geometries but without field properties. + +@module /workspace/templates/mvt_geom +*/ module.exports = _ => { const @@ -25,4 +32,4 @@ module.exports = _ => { ${_.geom} ) ) tile` -} \ No newline at end of file +} diff --git a/mod/workspace/templates/wkt.js b/mod/workspace/templates/wkt.js index d23851083..c86ccea6b 100644 --- a/mod/workspace/templates/wkt.js +++ b/mod/workspace/templates/wkt.js @@ -1,3 +1,10 @@ +/** +### /workspace/templates/wkt + +The wkt layer query template returns feature records from a layer table as an ordered array. + +@module /workspace/templates/wkt +*/ module.exports = _ => { // Get fields array from query params. diff --git a/tests/mod/query.test.mjs b/tests/mod/query.test.mjs index 81bd60461..f2b4b3f1f 100644 --- a/tests/mod/query.test.mjs +++ b/tests/mod/query.test.mjs @@ -54,5 +54,14 @@ export async function queryTest() { const results = await mapp.utils.xhr(`/test/api/query?template=bogus_data_array`); codi.assertTrue(results instanceof Error, 'We should return an error for a bogus DBS connection'); }); + + /** + * @description Query: Testing a query with a bogus dbs on the template + * @function it + */ + await codi.it('Query: Testing a query with a bogus dbs on the template', async () => { + const results = await mapp.utils.xhr(`/test/api/query?template=cluster`); + codi.assertTrue(results instanceof Error, 'We should get an error because we didnt provide a layer param'); + }); }); } \ No newline at end of file