Skip to content

Commit

Permalink
layer query and templates (#1588)
Browse files Browse the repository at this point in the history
* layer query and templates

* check whether layer template is instance of error

* parse layer param as string

* Parse error message as string on return

* remove string parser

* update layer queries

* add test

---------

Co-authored-by: Rob Hurst <[email protected]>
Co-authored-by: Robert Hurst <[email protected]>
  • Loading branch information
3 people authored Nov 1, 2024
1 parent 183eb8e commit 2e86724
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 120 deletions.
14 changes: 12 additions & 2 deletions mod/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand All @@ -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
Expand Down
22 changes: 19 additions & 3 deletions mod/workspace/getLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -15,13 +16,17 @@ const mergeTemplates = require('./mergeTemplates')

const getLocale = require('./getLocale')

const getTemplate = require('./getTemplate')

/**
@function getLayer
@async
@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.
Expand All @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions mod/workspace/templates/_queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
template: require('./gaz_query'),
},
get_last_location: {
layer: true,
render: require('./get_last_location'),
},
distinct_values: {
Expand All @@ -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: {
Expand All @@ -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
}
Expand Down
34 changes: 19 additions & 15 deletions mod/workspace/templates/cluster.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -38,5 +43,4 @@ module.exports = _ => {
WHERE TRUE ${where} \${filter}) grid
GROUP BY x_round, y_round;`

}
}
123 changes: 62 additions & 61 deletions mod/workspace/templates/cluster_hex.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -132,9 +135,7 @@ module.exports = _ => {
END
END as point
FROM first
) AS grid
GROUP BY point;`
FROM first) AS grid
}
GROUP BY point;`
}
Loading

0 comments on commit 2e86724

Please sign in to comment.