mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
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:
+11
-11
@@ -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
@@ -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 }
|
||||
];
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user