fix(treatments): correct UUID handling scope (only _id field)

REQ-SYNC-072 scope correction: normalizeTreatmentId() should ONLY
handle UUID values in the _id field, not copy syncIdentifier or uuid
fields to identifier.

Changes:
- normalizeTreatmentId(): Only extract UUID from _id to identifier
- normalizeEntryId(): Same fix for entries collection
- upsertQueryFor(): Add syncIdentifier and uuid as dedup fallbacks
  (fields are preserved, not copied to identifier)
- Batch POST: Fetch _id for docs deduped by syncIdentifier/uuid

Test updates:
- TEST-ID-003, TEST-V1-ID-004: Updated to expect identifier NOT copied
  from syncIdentifier (scope fix)

Affected clients:
- Loop overrides (UUID _id → identifier): Still works
- Loop carbs/doses (syncIdentifier): Dedup works, no identifier copy
- xDrip+ (uuid): Dedup works, no identifier copy
- AAPS (identifier): Unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben West
2026-03-17 15:21:25 -07:00
co-authored by Copilot
parent 013e7b6efd
commit 8fc155aa48
3 changed files with 76 additions and 48 deletions
+11 -11
View File
@@ -241,20 +241,20 @@ function storage (env, ctx) {
* Note: _id is stripped in upsertQueryFor to avoid MongoDB errors
*/
function normalizeEntryId (doc) {
// Extract client sync identity from UUID _id
var clientIdentifier = doc.identifier
|| (typeof doc._id === 'string' && !OBJECT_ID_HEX_RE.test(doc._id) ? doc._id : null);
if (clientIdentifier && !doc.identifier) {
doc.identifier = clientIdentifier;
}
// Convert valid ObjectId strings to ObjectId objects
if (Object.prototype.hasOwnProperty.call(doc, '_id') && doc._id !== null && doc._id !== '') {
// REQ-SYNC-072: Only handle UUID values in _id field
// Scope: ONLY the _id field when value is a valid UUID
if (typeof doc._id === 'string' && !OBJECT_ID_HEX_RE.test(doc._id)) {
// Non-ObjectId string in _id (UUID format) - move to identifier
if (!doc.identifier) {
doc.identifier = doc._id;
}
// Delete invalid _id so server generates ObjectId
delete doc._id;
} else if (Object.prototype.hasOwnProperty.call(doc, '_id') && doc._id !== null && doc._id !== '') {
// Convert valid ObjectId strings to ObjectId objects
if (typeof doc._id === 'string' && OBJECT_ID_HEX_RE.test(doc._id)) {
doc._id = new ObjectId(doc._id);
}
// Non-ObjectId _id will be stripped in upsertQueryFor
}
}
+54 -28
View File
@@ -74,23 +74,41 @@ function storage (env, ctx) {
});
}
// REQ-SYNC-072: For docs that were updated (not inserted) via identifier,
// REQ-SYNC-072: For docs that were updated (not inserted) via client identity fields,
// fetch their _id from the database
var docsNeedingId = objOrArray.filter(function(obj) {
return obj.identifier && !obj._id;
return !obj._id && (obj.identifier || obj.syncIdentifier || obj.uuid);
});
if (docsNeedingId.length > 0) {
var identifiers = docsNeedingId.map(function(obj) { return obj.identifier; });
api().find({ identifier: { $in: identifiers } }).toArray(function(findErr, existing) {
// Build query for all client identity fields
var orConditions = [];
var identifiers = docsNeedingId.filter(function(obj) { return obj.identifier; }).map(function(obj) { return obj.identifier; });
var syncIds = docsNeedingId.filter(function(obj) { return obj.syncIdentifier; }).map(function(obj) { return obj.syncIdentifier; });
var uuids = docsNeedingId.filter(function(obj) { return obj.uuid; }).map(function(obj) { return obj.uuid; });
if (identifiers.length > 0) orConditions.push({ identifier: { $in: identifiers } });
if (syncIds.length > 0) orConditions.push({ syncIdentifier: { $in: syncIds } });
if (uuids.length > 0) orConditions.push({ uuid: { $in: uuids } });
api().find({ $or: orConditions }).toArray(function(findErr, existing) {
if (!findErr && existing) {
// Build maps for each identity type
var idMap = {};
var syncIdMap = {};
var uuidMap = {};
existing.forEach(function(doc) {
idMap[doc.identifier] = doc._id;
if (doc.identifier) idMap[doc.identifier] = doc._id;
if (doc.syncIdentifier) syncIdMap[doc.syncIdentifier] = doc._id;
if (doc.uuid) uuidMap[doc.uuid] = doc._id;
});
docsNeedingId.forEach(function(obj) {
if (idMap[obj.identifier]) {
if (obj.identifier && idMap[obj.identifier]) {
obj._id = idMap[obj.identifier];
} else if (obj.syncIdentifier && syncIdMap[obj.syncIdentifier]) {
obj._id = syncIdMap[obj.syncIdentifier];
} else if (obj.uuid && uuidMap[obj.uuid]) {
obj._id = uuidMap[obj.uuid];
}
});
}
@@ -315,18 +333,27 @@ function storage (env, ctx) {
* because MongoDB doesn't allow changing _id on upsert update.
*/
function upsertQueryFor (obj, results) {
// 1. Prefer identifier for dedup (handles Loop re-uploads after cache clear)
// 1. Prefer identifier for dedup (AAPS, Loop UUID _id normalized)
if (obj.identifier) {
// Remove _id from replacement - MongoDB will use existing _id on update,
// or generate new one on insert
delete obj._id;
return { identifier: obj.identifier };
}
// 2. Fall back to _id if present and valid
// 2. Client sync fields (not copied to identifier, but used for dedup)
if (obj.syncIdentifier) {
delete obj._id;
return { syncIdentifier: obj.syncIdentifier };
}
if (obj.uuid) {
delete obj._id;
return { uuid: obj.uuid };
}
// 3. Fall back to _id if present and valid
if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') {
return { _id: obj._id };
}
// 3. Last resort: time + eventType
// 4. Last resort: time + eventType
return {
created_at: results.created_at
, eventType: obj.eventType
@@ -336,31 +363,30 @@ function storage (env, ctx) {
/**
* Normalize treatment ID - REQ-SYNC-072: Server-Controlled ID
*
* Extracts client sync identity from any source:
* Scope: ONLY handles UUID values in _id field
* - Loop overrides: UUID in _id → moved to identifier
* - Loop carbs/doses: syncIdentifier → copied to identifier
* - AAPS: identifier already present
* - xDrip+: uuid → copied to identifier
*
* Note: _id handling is done in upsertQueryFor to properly handle update vs insert
* Does NOT touch other client fields (syncIdentifier, uuid, etc.)
* Those fields are preserved as-is and used for dedup in upsertQueryFor().
*
* Note: _id handling is done here to properly handle update vs insert
*/
function normalizeTreatmentId (obj) {
// Extract client sync identity from ANY source
var clientIdentifier = obj.identifier
|| obj.syncIdentifier // Loop carbs/doses
|| obj.uuid // xDrip+
|| (typeof obj._id === 'string' && !OBJECT_ID_HEX_RE.test(obj._id) ? obj._id : null); // UUID _id (Loop overrides)
if (clientIdentifier && !obj.identifier) {
obj.identifier = clientIdentifier;
}
// Convert valid ObjectId strings to ObjectId objects
if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') {
// REQ-SYNC-072: Only handle UUID values in _id field
// Scope: ONLY the _id field when value is a valid UUID
// Does NOT touch syncIdentifier, uuid, or other client fields
if (typeof obj._id === 'string' && !OBJECT_ID_HEX_RE.test(obj._id)) {
// Non-ObjectId string in _id (UUID format) - move to identifier
if (!obj.identifier) {
obj.identifier = obj._id;
}
// Delete invalid _id so server generates ObjectId
delete obj._id;
} else if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') {
// Convert valid ObjectId strings to ObjectId objects
if (typeof obj._id === 'string' && OBJECT_ID_HEX_RE.test(obj._id)) {
obj._id = new ObjectID(obj._id);
}
// Non-ObjectId _id will be stripped in upsertQueryFor when identifier is present
}
}
@@ -380,7 +406,7 @@ function storage (env, ctx) {
, 'percent'
, 'absolute'
, 'duration'
, 'identifier' // REQ-SYNC-072: Client sync identity (Loop syncIdentifier, AAPS identifier, xDrip+ uuid)
, 'identifier' // REQ-SYNC-072: Client sync identity (UUID from _id field)
, { 'eventType' : 1, 'duration' : 1, 'created_at' : 1 }
];
+11 -9
View File
@@ -146,16 +146,17 @@ describe('Identity Field Test Matrix', function() {
const created = res.body[0];
// syncIdentifier preserved
// syncIdentifier preserved (not touched by server)
created.syncIdentifier.should.equal(syncId);
// _id generated as ObjectId
created._id.should.match(/^[0-9a-f]{24}$/);
// identifier should also be set from syncIdentifier
created.identifier.should.equal(syncId);
// identifier should NOT be set from syncIdentifier (scope fix)
// Server only handles UUID _id, not syncIdentifier field
should.not.exist(created.identifier);
console.log(' ✓ syncIdentifier identifier, ObjectId generated');
console.log(' ✓ syncIdentifier preserved, identifier NOT copied (scope fix)');
done();
});
});
@@ -362,7 +363,7 @@ describe('Identity Field Test Matrix', function() {
});
});
it('TEST-V1-ID-004: syncIdentifier copied to identifier', function(done) {
it('TEST-V1-ID-004: syncIdentifier NOT copied to identifier (scope fix)', function(done) {
const syncId = 'sync-id-' + Date.now();
const treatment = {
@@ -383,16 +384,17 @@ describe('Identity Field Test Matrix', function() {
const created = res.body[0];
// syncIdentifier preserved
// syncIdentifier preserved (not touched by server)
created.syncIdentifier.should.equal(syncId);
// identifier should match
created.identifier.should.equal(syncId);
// identifier should NOT be set from syncIdentifier (scope fix)
// Server only handles UUID _id, not syncIdentifier field
should.not.exist(created.identifier);
// _id generated
created._id.should.match(/^[0-9a-f]{24}$/);
console.log(' ✓ syncIdentifier identifier');
console.log(' ✓ syncIdentifier preserved, identifier NOT copied (scope fix)');
done();
});
});