feat: add UUID_HANDLING feature flag for GET/DELETE by identifier

- env.js: Add UUID_HANDLING env var (default: false)
- query.js: Add UUID detection in normalizeIdValue()
  - When UUID_HANDLING=true and _id is UUID, search by identifier field
  - Returns searchByIdentifier flag to redirect query
- treatments.js: Move queryOpts inside query_for() for env access
- entries.js: Same pattern for entries collection

When UUID_HANDLING=true:
- GET /treatments/{uuid} searches by identifier field
- DELETE /treatments/{uuid} deletes by identifier field
- Same behavior for entries collection

When UUID_HANDLING=false (default):
- UUID _id values return empty results (safe, no crash)
- Maintains backwards compatibility

Refs: uuid-feature-flag, uuid-query-impl from uuid-identifier-lookup.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben West
2026-03-17 13:21:14 -07:00
co-authored by Copilot
parent 4e79654d9f
commit bf6cfb7730
4 changed files with 66 additions and 35 deletions
+15 -14
View File
@@ -163,7 +163,21 @@ function storage (env, ctx) {
}
function query_for (opts) {
return find_options(opts, storage.queryOpts);
// Build queryOpts inside function to access env.uuidHandling
var queryOpts = {
walker: {
date: parseInt
, sgv: parseInt
, filtered: parseInt
, unfiltered: parseInt
, rssi: parseInt
, noise: parseInt
, mbg: parseInt
}
, useEpoch: true
, uuidHandling: env.uuidHandling
};
return find_options(opts, queryOpts);
}
// closure to represent the API
@@ -247,19 +261,6 @@ function storage (env, ctx) {
return api;
}
storage.queryOpts = {
walker: {
date: parseInt
, sgv: parseInt
, filtered: parseInt
, unfiltered: parseInt
, rssi: parseInt
, noise: parseInt
, mbg: parseInt
}
, useEpoch: true
};
// expose module
storage.storage = storage;
module.exports = storage;
+5
View File
@@ -75,6 +75,11 @@ function setSSL () {
env.secureHstsHeaderPreload = readENVTruthy("SECURE_HSTS_HEADER_PRELOAD", false);
env.secureCsp = readENVTruthy("SECURE_CSP", false);
env.secureCspReportOnly = readENVTruthy("SECURE_CSP_REPORT_ONLY", false);
// UUID handling for AID clients (Loop, Trio, AAPS, xDrip+)
// When true: UUID _id values are normalized to 'identifier' field
// When false: UUID _id values are rejected on write, ignored on read
env.uuidHandling = readENVTruthy("UUID_HANDLING", false);
}
// A little ugly, but we don't want to read the secret into a var
+32 -8
View File
@@ -4,6 +4,7 @@ const traverse = require('traverse');
const ObjectID = require('mongodb-legacy').ObjectId;
const moment = require('moment');
const OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const TWO_DAYS = 172800000;
/**
@@ -89,32 +90,55 @@ function enforceDateFilter (query, opts) {
/**
* Helper to set ObjectID type for `_id` queries.
* Forces anything named `_id` to be the `ObjectID` type.
* When opts.uuidHandling is true, UUID _id values search by identifier field.
*/
function updateIdQuery (query) {
function updateIdQuery (query, opts) {
if (!Object.prototype.hasOwnProperty.call(query, '_id')) {
return;
}
if (typeof query._id === 'string') {
query._id = normalizeIdValue(query._id);
var result = normalizeIdValue(query._id, opts);
if (result.searchByIdentifier) {
// UUID detected with uuidHandling enabled - search by identifier instead
query.identifier = result.value;
delete query._id;
} else {
query._id = result.value;
}
return;
}
if (query._id && typeof query._id === 'object') {
traverse(query._id).forEach(function (x) {
if (this.isLeaf) {
this.update(normalizeIdValue(x));
var result = normalizeIdValue(x, opts);
// For complex queries (like $in), we only handle ObjectIDs
// UUID handling in complex queries would require more work
this.update(result.value);
}
});
}
}
function normalizeIdValue (value) {
/**
* Normalize an _id value for MongoDB queries.
* @param {string} value - The _id value to normalize
* @param {Object} opts - Options including uuidHandling flag
* @returns {Object} { value: normalized, searchByIdentifier: boolean }
*/
function normalizeIdValue (value, opts) {
if (typeof value === 'string' && OBJECT_ID_HEX_RE.test(value)) {
return new ObjectID(value);
return { value: new ObjectID(value), searchByIdentifier: false };
}
return value;
// Check if it's a UUID and uuidHandling is enabled
if (typeof value === 'string' && UUID_RE.test(value) && opts && opts.uuidHandling) {
return { value: value, searchByIdentifier: true };
}
// Unknown format - return as-is (will likely return 0 results)
return { value: value, searchByIdentifier: false };
}
/**
@@ -142,8 +166,8 @@ function create (params, opts) {
enforceDateFilter(query, opts);
}
// Help queries for _id.
updateIdQuery(query);
// Help queries for _id (pass opts for UUID handling)
updateIdQuery(query, opts);
//console.info('query:', query);
// Ready for mongodb.find( ) and friends.
+14 -13
View File
@@ -216,7 +216,20 @@ function storage (env, ctx) {
}
function query_for (opts) {
return find_options(opts, storage.queryOpts);
// Build queryOpts inside function to access env.uuidHandling
var queryOpts = {
walker: {
insulin: parseInt
, carbs: parseInt
, glucose: parseInt
, notes: find_options.parseRegEx
, eventType: find_options.parseRegEx
, enteredBy: find_options.parseRegEx
}
, dateField: 'created_at'
, uuidHandling: env.uuidHandling
};
return find_options(opts, queryOpts);
}
function remove (opts, fn) {
@@ -459,16 +472,4 @@ function prepareData(obj) {
return results;
}
storage.queryOpts = {
walker: {
insulin: parseInt
, carbs: parseInt
, glucose: parseInt
, notes: find_options.parseRegEx
, eventType: find_options.parseRegEx
, enteredBy: find_options.parseRegEx
}
, dateField: 'created_at'
};
module.exports = storage;