From 2f33fd580afacf14d14799077a0e6fda085b460a Mon Sep 17 00:00:00 2001 From: brontolosone <177225737+brontolosone@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:56:43 +0000 Subject: [PATCH 1/2] allow for generation of weak etags --- lib/util/http.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/http.js b/lib/util/http.js index fba432035..ac3ce8b2b 100644 --- a/lib/util/http.js +++ b/lib/util/http.js @@ -119,9 +119,9 @@ const url = (strings, ...parts) => { // Checks Etag in the request if it matches serverEtag then returns 304 // Otherwise executes given function 'fn' -const withEtag = (serverEtag, fn) => (request, response) => { +const withEtag = (serverEtag, fn, isWeak=false) => (request, response) => { - response.set('ETag', `"${serverEtag}"`); + response.set('ETag', `${isWeak ? 'W/': ''}"${serverEtag}"`); // Etag logic inspired from https://stackoverflow.com/questions/72334843/custom-computed-etag-for-express-js/72335674#72335674 const clientEtag = request.get('If-None-Match'); From b56d56c2c9974da0e222869db71179914c9e759d Mon Sep 17 00:00:00 2001 From: brontolosone <177225737+brontolosone@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:55:01 +0000 Subject: [PATCH 2/2] eventcounter-etag-based revalidation for geodata endpoints --- lib/model/query/actees.js | 6 +++++- lib/resources/geo-extracts.js | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/model/query/actees.js b/lib/model/query/actees.js index acaedb40b..6e9e399c7 100644 --- a/lib/model/query/actees.js +++ b/lib/model/query/actees.js @@ -16,5 +16,9 @@ const provision = (species, parent) => ({ one }) => one(sql`insert into actees (id, species, parent) values (${uuid()}, ${species}, ${(parent == null) ? null : parent.acteeId}) returning *`) .then(construct(Actee)); -module.exports = { provision }; +const getEventCount = (acteeId) => ({ oneFirst }) => oneFirst(sql` + SELECT coalesce(sum(evt_count), 0) FROM eventcounters WHERE "acteeId" = ${acteeId} +`); + +module.exports = { provision, getEventCount }; diff --git a/lib/resources/geo-extracts.js b/lib/resources/geo-extracts.js index 4f783ad91..111ed2360 100644 --- a/lib/resources/geo-extracts.js +++ b/lib/resources/geo-extracts.js @@ -10,7 +10,7 @@ const { getOrNotFound } = require('../util/promise'); const { Form } = require('../model/frames'); const { Sanitize } = require('../util/param-sanitize'); -const { isTrue, json } = require('../util/http'); +const { isTrue, json, withEtag } = require('../util/http'); const Problem = require('../util/problem'); @@ -37,13 +37,15 @@ module.exports = (service, endpoint) => { // Bulk endpoints - service.get('/projects/:projectId/forms/:xmlFormId/submissions.geojson', endpoint.plain(async ({ Forms, GeoExtracts }, { auth, query, params }) => { + service.get('/projects/:projectId/forms/:xmlFormId/submissions.geojson', endpoint.plain(async ({ Forms, GeoExtracts, Actees }, { auth, query, params }) => { const form = await Forms.getByProjectAndXmlFormId(params.projectId, params.xmlFormId, Form.WithoutDef, Form.WithoutXml) .then(getOrNotFound) .then((foundForm) => auth.canOrReject('submission.list', foundForm)); - return GeoExtracts.getSubmissionFeatureCollectionGeoJson( + const acteeVersion = await Actees.getEventCount(form.acteeId); + + const createResponse = () => GeoExtracts.getSubmissionFeatureCollectionGeoJson( form.id, Sanitize.queryParamToArray(query.submissionID), Sanitize.queryParamToArray(query.fieldpath), @@ -53,15 +55,21 @@ module.exports = (service, endpoint) => { isTrue(query.deleted), Number.parseInt(query.limit, 10) || null, ).then(json); + + // Weak etag, as the order in the resultset is undefined. + return withEtag(acteeVersion, createResponse, true); })); - service.get('/projects/:projectId/datasets/:datasetName/entities.geojson', endpoint.plain(async ({ Datasets, GeoExtracts }, { auth, query, params }) => { + + service.get('/projects/:projectId/datasets/:datasetName/entities.geojson', endpoint.plain(async ({ Datasets, GeoExtracts, Actees }, { auth, query, params }) => { const foundDataset = await Datasets.get(params.projectId, params.datasetName, true) .then(getOrNotFound) .then((dataset) => auth.canOrReject('entity.list', dataset)); - return GeoExtracts.getEntityFeatureCollectionGeoJson( + const acteeVersion = await Actees.getEventCount(foundDataset.acteeId); + + const createResponse = () => GeoExtracts.getEntityFeatureCollectionGeoJson( foundDataset.id, Sanitize.queryParamToUuidArray(query.entityUUID, 'entityUUID'), Sanitize.queryParamToIntArray(query.creatorId, 'creatorId'), @@ -70,6 +78,9 @@ module.exports = (service, endpoint) => { isTrue(query.deleted), Number.parseInt(query.limit, 10) || null, ).then(json); + + // Weak etag, as the order in the resultset is undefined. + return withEtag(acteeVersion, createResponse, true); })); };