Skip to content

Commit e86d3b5

Browse files
committed
query to check for non-contiguous interrupted branches
1 parent 0ebebd9 commit e86d3b5

File tree

2 files changed

+175
-2
lines changed

2 files changed

+175
-2
lines changed

lib/model/query/entities.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss
308308
if (conflict !== ConflictType.HARD) { // We don't want to downgrade conflict here
309309
conflict = conflictingProperties.length > 0 ? ConflictType.HARD : ConflictType.SOFT;
310310
}
311+
} else {
312+
// This may still be a soft conflict if the new version is not contiguous with this branch's trunk version
313+
const interrupted = await Entities._interruptedBranch(serverEntity.id, clientEntity);
314+
if (interrupted) {
315+
conflict = ConflictType.SOFT;
316+
}
311317
}
312318

313319
// merge data
@@ -541,6 +547,29 @@ const processSubmissionEvent = (event, parentEvent) => (container) =>
541547
////////////////////////////////////////////////////////////////////////////////
542548
// Submission processing helper functions
543549

550+
// Used by _updateEntity to determine if a new offline update is contiguous with its trunk version
551+
// by searching for an interrupting version with a different or null branchId that has a higher
552+
// version than the trunk version of the given branch.
553+
const _interruptedBranch = (entityId, clientEntity) => async ({ maybeOne }) => {
554+
// If there is no branchId, the branch cannot be interrupted
555+
if (clientEntity.def.branchId == null)
556+
return false;
557+
558+
// look for a version of a different branch that has a version
559+
// higher than the trunkVersion, which indicates an interrupting version.
560+
// if trunkVersion is null (becuase it is part of a branch beginning with
561+
// an offline create), look for a version higher than 1 because version
562+
// 1 is implicitly the create action of that offline branch.
563+
const interruptingVersion = await maybeOne(sql`
564+
SELECT version
565+
FROM entity_defs
566+
WHERE "branchId" IS DISTINCT FROM ${clientEntity.def.branchId}
567+
AND version > ${clientEntity.def.trunkVersion || 1}
568+
AND "entityId" = ${entityId}
569+
LIMIT 1`);
570+
return interruptingVersion.isDefined();
571+
};
572+
544573
// Used by _computeBaseVersion to hold submissions that are not yet ready to be processed
545574
const _holdSubmission = (eventId, submissionId, submissionDefId, entityUuid, branchId, branchBaseVersion) => async ({ run }) => run(sql`
546575
INSERT INTO entity_submission_backlog ("auditId", "submissionId", "submissionDefId", "entityUuid", "branchId", "branchBaseVersion", "loggedAt")
@@ -780,7 +809,7 @@ module.exports = {
780809
createSource,
781810
createMany,
782811
_createEntity, _updateEntity,
783-
_computeBaseVersion,
812+
_computeBaseVersion, _interruptedBranch,
784813
_holdSubmission, _checkHeldSubmission,
785814
_getNextHeldSubmissionInBranch, _deleteHeldSubmissionByEventId,
786815
_getHeldSubmissionsAsEvents,

test/integration/api/offline-entities.js

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,7 @@ describe('Offline Entities', () => {
13801380
});
13811381
}));
13821382

1383-
it('should mark an update that is not contiguous with its trunk version as a soft conflict', testOfflineEntities(async (service, container) => {
1383+
it('should mark an update that is not contiguous (due to force processing) as a soft conflict', testOfflineEntities(async (service, container) => {
13841384
const asAlice = await service.login('alice');
13851385
const branchId = uuid();
13861386

@@ -1427,6 +1427,150 @@ describe('Offline Entities', () => {
14271427
versions.map(v => v.conflict).should.eql([null, null, 'hard', 'soft']);
14281428
});
14291429
}));
1430+
1431+
it('should mark an update that is not contiguous with its trunk version as a soft conflict on entity despite earlier conflict resolution', testOfflineEntities(async (service, container) => {
1432+
const asAlice = await service.login('alice');
1433+
const branchId = uuid();
1434+
1435+
// Update existing entity on server (change age from 22 to 24)
1436+
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?baseVersion=1')
1437+
.send({ data: { age: '24' } })
1438+
.expect(200);
1439+
1440+
// Send update (change status from null to arrived)
1441+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1442+
.send(testData.instances.offlineEntity.one
1443+
.replace('branchId=""', `branchId="${branchId}"`)
1444+
)
1445+
.set('Content-Type', 'application/xml')
1446+
.expect(200);
1447+
await exhaust(container);
1448+
1449+
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?resolve=true&baseVersion=3')
1450+
.expect(200);
1451+
1452+
// Send second update (change age from 22 to 26)
1453+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1454+
.send(testData.instances.offlineEntity.one
1455+
.replace('branchId=""', `branchId="${branchId}"`)
1456+
.replace('one', 'one-update2')
1457+
.replace('baseVersion="1"', 'baseVersion="2"')
1458+
.replace('<status>arrived</status>', '<age>26</age>')
1459+
)
1460+
.set('Content-Type', 'application/xml')
1461+
.expect(200);
1462+
await exhaust(container);
1463+
1464+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/versions')
1465+
.then(({ body: versions }) => {
1466+
versions.map(v => v.conflict).should.eql([null, null, 'soft', 'soft']);
1467+
});
1468+
1469+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
1470+
.then(({ body: entity }) => {
1471+
should(entity.conflict).equal('soft');
1472+
});
1473+
}));
1474+
1475+
it('should mark an update that is not contiguous (from an offline create branch) as a soft conflict on entity despite earlier conflict resolution', testOfflineEntities(async (service, container) => {
1476+
const asAlice = await service.login('alice');
1477+
const branchId = uuid();
1478+
1479+
// Send initial submission to create entity
1480+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1481+
.send(testData.instances.offlineEntity.two)
1482+
.set('Content-Type', 'application/xml')
1483+
.expect(200);
1484+
await exhaust(container);
1485+
1486+
// Update existing entity on server before getting the rest of the branch
1487+
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd?baseVersion=1')
1488+
.send({ data: { age: '24' } })
1489+
.expect(200);
1490+
1491+
// Send update (change status from new to arrived)
1492+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1493+
.send(testData.instances.offlineEntity.two
1494+
.replace('two', 'two-update1')
1495+
.replace('branchId=""', `branchId="${branchId}"`)
1496+
.replace('create="1"', 'update="1"')
1497+
.replace('baseVersion=""', 'baseVersion="1"')
1498+
.replace('<status>new</status>', '<status>arrived</status>')
1499+
)
1500+
.set('Content-Type', 'application/xml')
1501+
.expect(200);
1502+
await exhaust(container);
1503+
1504+
// Conflict is hard here
1505+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd')
1506+
.then(({ body: entity }) => {
1507+
should(entity.conflict).equal('hard');
1508+
});
1509+
1510+
// resolve the conflict
1511+
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd?resolve=true&baseVersion=3')
1512+
.expect(200);
1513+
1514+
// Send second update in offline create-update-update chain (change age from 22 to 26)
1515+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1516+
.send(testData.instances.offlineEntity.two
1517+
.replace('two', 'two-update2')
1518+
.replace('branchId=""', `branchId="${branchId}"`)
1519+
.replace('create="1"', 'update="1"')
1520+
.replace('baseVersion=""', 'baseVersion="2"')
1521+
.replace('<status>new</status>', '<status>arrived</status>')
1522+
.replace('<age>20</age>', '<age>27</age>')
1523+
)
1524+
.set('Content-Type', 'application/xml')
1525+
.expect(200);
1526+
await exhaust(container);
1527+
1528+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd/versions')
1529+
.then(({ body: versions }) => {
1530+
versions.map(v => v.conflict).should.eql([null, null, 'hard', 'soft']);
1531+
});
1532+
1533+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd')
1534+
.then(({ body: entity }) => {
1535+
should(entity.conflict).equal('soft');
1536+
});
1537+
}));
1538+
1539+
it('should check that interrupting version logic is doesnt flag non-conflicts as conflicts', testOfflineEntities(async (service, container) => {
1540+
const asAlice = await service.login('alice');
1541+
const branchId = uuid();
1542+
1543+
// Send initial submission to create entity
1544+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1545+
.send(testData.instances.offlineEntity.two)
1546+
.set('Content-Type', 'application/xml')
1547+
.expect(200);
1548+
await exhaust(container);
1549+
1550+
// Send second update in offline create-update-update chain (change age from 22 to 26)
1551+
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
1552+
.send(testData.instances.offlineEntity.two
1553+
.replace('two', 'two-update')
1554+
.replace('branchId=""', `branchId="${branchId}"`)
1555+
.replace('create="1"', 'update="1"')
1556+
.replace('baseVersion=""', 'baseVersion="1"')
1557+
.replace('<status>new</status>', '<status>arrived</status>')
1558+
.replace('<age>20</age>', '<age>27</age>')
1559+
)
1560+
.set('Content-Type', 'application/xml')
1561+
.expect(200);
1562+
await exhaust(container);
1563+
1564+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd/versions')
1565+
.then(({ body: versions }) => {
1566+
versions.map(v => v.conflict).should.eql([null, null]);
1567+
});
1568+
1569+
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd')
1570+
.then(({ body: entity }) => {
1571+
should(entity.conflict).equal(null);
1572+
});
1573+
}));
14301574
});
14311575

14321576
describe('locking an entity while processing a related submission', function() {

0 commit comments

Comments
 (0)