Skip to content

Commit

Permalink
Mark soft conflict if not contiguous with trunk
Browse files Browse the repository at this point in the history
  • Loading branch information
ktuite committed Sep 16, 2024
1 parent 3ae8a69 commit 0b162ff
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 1 deletion.
85 changes: 84 additions & 1 deletion lib/data/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,64 @@ const diffEntityData = (defData) => {
return diffs;
};

// Copied from frontend though it may not all be necessary
// Offline branch
class Branch {
// firstUpdate is the first offline update (not create) to be processed from
// the branch. entityRoot is the first version of the entity.
constructor(firstUpdate, entityRoot) {
if (firstUpdate.trunkVersion != null) {
// The first version from the branch to be processed (not necessarily the
// first in the original branch order)
this.first = firstUpdate;

// How many versions that have been processed are from the branch?
this.length = 1;

// Was this.first processed in branch order, or was it processed before an
// earlier change in the branch?
const { trunkVersion } = firstUpdate;
this.firstInOrder = firstUpdate.branchBaseVersion === trunkVersion;

/* this.lastContiguousWithTrunk is the version number of the last version
from the branch that is contiguous with the trunk version. In other words,
it is the version number of the last version where there has been no
update from outside the branch between the version and the trunk version.
this.lastContiguousWithTrunk is not related to branch order: as long as
there hasn't been an update from outside the branch, the branch is
contiguous, regardless of the order of the updates within it. */
this.lastContiguousWithTrunk = firstUpdate.version === trunkVersion + 1
? firstUpdate.version
: 0;
} else {
// If the entity was both created and updated offline before being sent to
// the server, then we treat the creation as part of the same branch as
// the update(s). The creation doesn't have a branch ID, but we treat it
// as part of the branch anyway.
this.first = entityRoot;
// If the submission for the entity creation was received late and
// processed out of order, then firstUpdate.version === 1. In that case,
// we can't reliably determine which entity version corresponds to the
// entity creation, so we don't treat the creation as part of the branch.
this.length = firstUpdate.version === 1 ? 1 : 2;
this.firstInOrder = this.length === 2;
this.lastContiguousWithTrunk = firstUpdate.version === 2 ? 2 : 1;
}

this.id = firstUpdate.branchId;
// The last version from the branch to be processed
this.last = firstUpdate;
}

add(version) {
this.length += 1;
this.last = version;
if (version.baseVersion === this.lastContiguousWithTrunk &&
version.version === version.baseVersion + 1)
this.lastContiguousWithTrunk = version.version;
}
}

// Returns an array of properties which are different between
// `dataReceived` and `otherVersionData`
const getDiffProp = (dataReceived, otherVersionData) =>
Expand All @@ -417,6 +475,26 @@ const getWithConflictDetails = (defs, audits, relevantToConflict) => {

const relevantBaseVersions = new Set();

// build up branches
const branches = new Map();
for (const version of defs) {
const { branchId } = version;
if (branchId != null && version.branchBaseVersion != null) {
const existingBranch = branches.get(branchId);
if (existingBranch == null) {
const newBranch = new Branch(version, defs[0]);
branches.set(branchId, newBranch);
version.branch = newBranch;
// If the entity was created offline, then add the branch to the
// entity creation.
newBranch.first.branch = newBranch;
} else {
existingBranch.add(version);
version.branch = existingBranch;
}
}
}

for (const def of defs) {

const v = mergeLeft(def.forApi(),
Expand All @@ -436,7 +514,12 @@ const getWithConflictDetails = (defs, audits, relevantToConflict) => {
v.source = event.source;

if (v.version > 1) { // v.root is false here - can use either
const conflict = v.version !== (v.baseVersion + 1);
let notContiguousWithTrunk = false;
if (v.branchId != null) {
notContiguousWithTrunk = branches.get(v.branchId).lastContiguousWithTrunk < v.baseVersion;
}

const conflict = v.version !== (v.baseVersion + 1) || notContiguousWithTrunk;

v.baseDiff = getDiffProp(v.dataReceived, { ...defs[v.baseVersion - 1].data, label: defs[v.baseVersion - 1].label });

Expand Down
86 changes: 86 additions & 0 deletions test/integration/api/offline-entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -1270,4 +1270,90 @@ describe('Offline Entities', () => {
}));
});
});

describe('conflict cases', () => {
it('should mark an update that is not contiguous with its trunk version as a soft conflict', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();

// Update existing entity on server (change age from 22 to 24)
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?baseVersion=1')
.send({ data: { age: '24' } })
.expect(200);

// Send update (change status from null to arrived)
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.one
.replace('branchId=""', `branchId="${branchId}"`)
)
.set('Content-Type', 'application/xml')
.expect(200);

// Send second update (change age from 22 to 26)
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.one
.replace('branchId=""', `branchId="${branchId}"`)
.replace('one', 'one-update2')
.replace('baseVersion="1"', 'baseVersion="2"')
.replace('<status>arrived</status>', '<age>26</age>')
)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/versions')
.then(({ body: versions }) => {
versions.map(v => v.conflict).should.eql([null, null, 'soft', 'soft']);
});
}));

it('should mark an update that is not contiguous with its trunk version as a soft conflict', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();

// Send second update first
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.one
.replace('branchId=""', `branchId="${branchId}"`)
.replace('one', 'one-update2')
.replace('baseVersion="1"', 'baseVersion="2"')
.replace('<status>arrived</status>', '<status>checked in</status>')
)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);
await container.Entities.processBacklog(true);

// Send first update now (it will be applied right away)
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.one
.replace('branchId=""', `branchId="${branchId}"`)
)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);

// Send fourth update
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.one
.replace('branchId=""', `branchId="${branchId}"`)
.replace('one', 'one-update4')
.replace('baseVersion="1"', 'baseVersion="4"')
.replace('<status>arrived</status>', '<status>departed</status>')
)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);
await container.Entities.processBacklog(true);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/versions')
.then(({ body: versions }) => {
versions.map(v => v.conflict).should.eql([null, null, 'hard', 'soft']);
});
}));
});
});

0 comments on commit 0b162ff

Please sign in to comment.