diff --git a/db/repos/changes.js b/db/repos/changes.js index 2102b70..7064053 100644 --- a/db/repos/changes.js +++ b/db/repos/changes.js @@ -1,4 +1,5 @@ const sql = require('../sql').changes +const e = require('express') const _ = require('../../helpers') class Changes { @@ -7,21 +8,26 @@ class Changes { this.pgp = pgp } - list({limit = '100', offset = '0', user_id = null, range = 'false'}) { - let user_filter + list({earliest = null, latest = null, user_id = null, range = 'false'}) { + earliest = _.parse_datetime(earliest) + latest = _.parse_datetime(latest) + const filters = ['NOT l.hidden'] + if (earliest) { + filters.push(`c.created_at >= '${earliest}'::timestamptz`) + } + if (latest) { + filters.push(`c.created_at < '${latest}'::timestamptz`) + } if (user_id) { + let user_filter if (range === 'true') { user_filter = `ST_INTERSECTS(l.location, (SELECT range FROM users u2 WHERE u2.id = ${parseInt(user_id)}))` } else { user_filter = `c.user_id = ${parseInt(user_id)}` } + filters.push(user_filter) } - const filters = ['NOT l.hidden', user_filter] - const values = { - limit: parseInt(limit), - offset: parseInt(offset), - where: filters.filter(Boolean).join(' AND ') - } + const values = { where: filters.join(' AND ') } return this.db.any(sql.list, values) } diff --git a/db/sql/changes/list.sql b/db/sql/changes/list.sql index a9056a9..f362a16 100644 --- a/db/sql/changes/list.sql +++ b/db/sql/changes/list.sql @@ -18,5 +18,3 @@ LEFT JOIN users u ON c.user_id = u.id WHERE ${where:raw} ORDER BY c.created_at DESC -LIMIT ${limit} -OFFSET ${offset} diff --git a/docs/openapi.yml b/docs/openapi.yml index fbd9da1..2ac43d5 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -256,15 +256,22 @@ paths: - key: [] token: [] parameters: - - name: limit + - name: earliest in: query - description: Maximum number of changes to return. + description: Earliest UTC datetime of change (inclusive). schema: - type: integer - minimum: 1 - default: 100 - nullable: true - - $ref: '#/components/parameters/offset' + type: string + format: date-time + default: null + example: '2024-11-09T00:00:00Z' + - name: latest + in: query + description: Latest UTC datetime of change (exclusive). + schema: + type: string + format: date-time + default: null + example: null - name: user_id in: query description: User ID of changes to return. diff --git a/docs/paths/locations~changes.yml b/docs/paths/locations~changes.yml index 7639661..30db275 100644 --- a/docs/paths/locations~changes.yml +++ b/docs/paths/locations~changes.yml @@ -11,15 +11,22 @@ get: - key: [] token: [] parameters: - - name: limit + - name: earliest in: query - description: Maximum number of changes to return. + description: Earliest UTC datetime of change (inclusive). schema: - type: integer - minimum: 1 - default: 100 - nullable: true - - $ref: ../components/parameters.yml#/offset + type: string + format: date-time + default: null + example: '2024-11-09T00:00:00Z' + - name: latest + in: query + description: Latest UTC datetime of change (exclusive). + schema: + type: string + format: date-time + default: null + example: null - name: user_id in: query description: User ID of changes to return. diff --git a/helpers.js b/helpers.js index 4385d26..072b7fc 100644 --- a/helpers.js +++ b/helpers.js @@ -72,6 +72,49 @@ _.parse_point = function(value) { return points } +/** + * Parse date. + * + * @param {string} value – Date in the format 'yyyy-mm-dd'. + * @returns {string} Date in the format 'yyyy-mm-dd' or null. + */ +_.parse_date = function(value) { + if (!value) { + return null + } + // Check that date is yyyy-mm-dd + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw Error(`Date not formatted as yyyy-mm-dd: ${value}`) + } + const date = new Date(value) + if (isNaN(date.getTime())) { + throw Error(`Invalid date: ${value}`) + } + return date.toISOString().substring(0, 10) +} + +/** + * Parse datetime. + * + * @param {string} value – Datetime in the format 'yyyy-mm-ddTHH:MM:SSZ' + * (decimal seconds are permitted). + * @returns {string} Datetime in the same format, or null. + */ +_.parse_datetime = function(value) { + if (!value) { + return null + } + // Check that datetime is correctly formatted + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(value)) { + throw Error(`Datetime not formatted as yyyy-mm-ddTHH:MM:SSZ: ${value}`) + } + const date = new Date(value) + if (isNaN(date.getTime())) { + throw Error(`Invalid datetime: ${value}`) + } + return value +} + /** * Convert bounding box to SQL condition. *