Skip to content

Commit

Permalink
Added relevantToConflict flag in entity diff API
Browse files Browse the repository at this point in the history
  • Loading branch information
sadiqkhoja committed Oct 16, 2023
1 parent 54b7316 commit 7787851
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 17 deletions.
46 changes: 40 additions & 6 deletions lib/data/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,20 +354,32 @@ const ConflictType = {
SOFT: 'soft', // discrepancy in version number but no overlaping properties
HARD: 'hard' // discrepancy in version number and one/more properties updated simultaneously
};
const getWithConflictDetails = (defs) => {
const getWithConflictDetails = (defs, audits, relevantToConflict) => {
const defMap = new Map(defs.map(d => [d.version, d]));

const result = [];

const lastResolveEvent = audits.findLast(a => a.action === 'entity.update.resolve');

const auditMap = new Map(audits.map(a => [a.details.entityDefId, a.details]));

const versionMap = new Map();

let lastGoodVersion = 0; // Entity Version is never 0

const relevantBaseVersions = new Set();

for (const def of defs) {
const v = mergeLeft(def,
const { sourceEvent, submissionCreate, submission } = auditMap.get(def.id);

const v = mergeLeft(def.forApi(),
{
conflict: null,
resolved: false,
baseDiff: [],
serverDiff: [],
conflictingProp: [],
source: {} // TODO
source: { sourceEvent, submissionCreate, submission },
lastGoodVersion: false
});

if (v.version > 1) { // v.root is false here - can use either
Expand All @@ -380,13 +392,35 @@ const getWithConflictDetails = (defs) => {
if ('label' in v.dataReceived && v.dataReceived.label !== defMap.get(v.version - 1).label) v.serverDiff.push('label');

if (conflict) {
v.conflict = v.conflictingProp && v.conflictingProp.length > 0 ? ConflictType.HARD : ConflictType.SOFT;
v.conflict = v.conflictingProperties && v.conflictingProperties.length > 0 ? ConflictType.HARD : ConflictType.SOFT;

v.resolved = lastResolveEvent && lastResolveEvent.details.entityDefId >= def.id;

// lastGoodVersion is the baseVersion of first unresolved conflict
if (!v.resolved && lastGoodVersion === 0) {
lastGoodVersion = v.baseVersion;

v.resolve = false; // TOOD
// baseVersion will be always in versionMap; if not then something terrible has happened - 500 error is fine
versionMap.get(v.baseVersion).lastGoodVersion = true;
}
}

// We have some unresolved conflicts
if (lastGoodVersion > 0) {
relevantBaseVersions.add(v.baseVersion);
}
}

versionMap.set(v.version, v);

result.push(v);
}

// We return all the versions after last good version and the base versions of those.
if (relevantToConflict) {
return result.filter(v => lastGoodVersion > 0 && (v.version >= lastGoodVersion || relevantBaseVersions.has(v.version)));
}

return result;
};

Expand Down
6 changes: 3 additions & 3 deletions lib/resources/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = (service, endpoint) => {
return withEtag(entity.aux.currentVersion.version, () => entity);
}));

service.get('/projects/:projectId/datasets/:name/entities/:uuid/versions', endpoint(async ({ Datasets, Entities, Audits }, { params, auth, queryOptions }) => {
service.get('/projects/:projectId/datasets/:name/entities/:uuid/versions', endpoint(async ({ Datasets, Entities, Audits }, { params, auth, queryOptions, query }) => {

const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);

Expand All @@ -47,9 +47,9 @@ module.exports = (service, endpoint) => {
// it means there's no entity with the provided UUID
if (defs.length === 0) return reject(Problem.user.notFound());

const audits = Audits.getByEntityId(defs[0].id, queryOptions);
const audits = await Audits.getByEntityId(defs[0].entityId, queryOptions);

return getWithConflictDetails(defs.map(d => d.forApi()), audits);
return getWithConflictDetails(defs, audits, isTrue(query.relevantToConflict));
}));

service.get('/projects/:projectId/datasets/:name/entities/:uuid/diffs', endpoint(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {
Expand Down
85 changes: 85 additions & 0 deletions test/integration/api/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,91 @@ describe('Entities API', () => {
versions[3].conflictingProperties.should.be.eql(['age', 'label']);
});
}));

describe('relevantToConflict', () => {

const createConflictOnV2 = async (user, container) => {
await user.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.send({ data: { age: '12' } })
.set('If-Match', '"1"')
.expect(200);

await user.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.send({ data: { age: '18' } })
.set('If-Match', '"2"')
.expect(200);

await user.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.updateEntity)
.set('Content-Type', 'application/xml')
.expect(200);

// Hard conflict - all properties are changed
await user.post('/v1/projects/1/forms/updateEntity/submissions')
.send(testData.instances.updateEntity.one.replace('baseVersion="1"', 'baseVersion="2"'))
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);
};
it('should return only relevent versions needed for conflict resolution', testEntities(async (service, container) => {
const asAlice = await service.login('alice');

await createConflictOnV2(asAlice, container);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/versions?relevantToConflict=true')
.expect(200)
.then(({ body: versions }) => {
// Doesn't return first version
versions.some(v => v.version === 1).should.be.false();

versions[0].lastGoodVersion.should.be.true();
versions[2].conflictingProperties.should.be.eql(['age']);
});
}));

it('should return empty array when all conflicts are resolved', testEntities(async (service, container) => {
const asAlice = await service.login('alice');

await createConflictOnV2(asAlice, container);

await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?resolve=true')
.expect(200);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/versions?relevantToConflict=true')
.expect(200)
.then(({ body: versions }) => {
versions.length.should.be.eql(0);
});
}));

it('should return only relevent versions after conflict resolution', testEntities(async (service, container) => {
const asAlice = await service.login('alice');

await createConflictOnV2(asAlice, container);

await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?resolve=true')
.expect(200);

await asAlice.post('/v1/projects/1/forms/updateEntity/submissions')
.send(testData.instances.updateEntity.two
.replace('baseVersion="1"', 'baseVersion="3"'))
.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?relevantToConflict=true')
.expect(200)
.then(({ body: versions }) => {
// Doesn't return old versions
versions.some(v => v.version < 3).should.be.false();

versions[0].lastGoodVersion.should.be.true();
versions[2].conflictingProperties.should.be.eql(['label']);
});
}));
});
});

describe('GET /datasets/:name/entities/:uuid/diffs', () => {
Expand Down
21 changes: 13 additions & 8 deletions test/unit/data/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const should = require('should');
const appRoot = require('app-root-path');
const assert = require('assert');
const { ConflictType } = require('../../../lib/data/entity');
const { Entity } = require('../../../lib/model/frames');
const { parseSubmissionXml, extractEntity, validateEntity, extractSelectedProperties, selectFields, diffEntityData, getDiffProp, getWithConflictDetails } = require(appRoot + '/lib/data/entity');
const { fieldsFor } = require(appRoot + '/test/util/schema');
const testData = require(appRoot + '/test/data/xml');
Expand Down Expand Up @@ -682,12 +683,14 @@ describe('extracting and validating entities', () => {

it('should fill in correct information for SOFT conflict', () => {
const defs = [
{ version: 1, label: 'John', data: { name: 'John', age: '88' }, dataReceived: { name: 'John', age: '88' }, conflictingProp: null, baseVersion: null },
{ version: 2, label: 'Jane', data: { name: 'Jane', age: '88' }, dataReceived: { name: 'Jane' }, conflictingProp: [], baseVersion: 1 },
{ version: 3, label: 'Jane', data: { name: 'Jane', age: '99' }, dataReceived: { age: '99' }, conflictingProp: [], baseVersion: 1 }
new Entity.Def({ id: 0, version: 1, label: 'John', data: { name: 'John', age: '88' }, dataReceived: { name: 'John', age: '88' }, conflictingProp: null, baseVersion: null }),
new Entity.Def({ id: 0, version: 2, label: 'Jane', data: { name: 'Jane', age: '88' }, dataReceived: { name: 'Jane' }, conflictingProp: [], baseVersion: 1 }),
new Entity.Def({ id: 0, version: 3, label: 'Jane', data: { name: 'Jane', age: '99' }, dataReceived: { age: '99' }, conflictingProp: [], baseVersion: 1 })
];

const result = getWithConflictDetails(defs);
const audits = [{ details: { entityDefId: 0 } }];

const result = getWithConflictDetails(defs, audits, false);

result[2].conflict.should.be.eql(ConflictType.SOFT);
result[2].baseDiff.should.be.eql(['age']);
Expand All @@ -696,12 +699,14 @@ describe('extracting and validating entities', () => {

it('should fill in correct information for HARD conflict', () => {
const defs = [
{ version: 1, label: 'John', data: { name: 'John', age: '88' }, dataReceived: { name: 'John', age: '88' }, conflictingProp: null, baseVersion: null },
{ version: 2, label: 'Jane', data: { name: 'Jane', age: '77' }, dataReceived: { age: '77' }, conflictingProp: [], baseVersion: 1 },
{ version: 3, label: 'Jane', data: { name: 'Jane', age: '99' }, dataReceived: { age: '99' }, conflictingProp: ['age'], baseVersion: 1 }
new Entity.Def({ id: 0, version: 1, label: 'John', data: { name: 'John', age: '88' }, dataReceived: { name: 'John', age: '88' }, conflictingProperties: null, baseVersion: null }),
new Entity.Def({ id: 0, version: 2, label: 'Jane', data: { name: 'Jane', age: '77' }, dataReceived: { age: '77' }, conflictingProperties: [], baseVersion: 1 }),
new Entity.Def({ id: 0, version: 3, label: 'Jane', data: { name: 'Jane', age: '99' }, dataReceived: { age: '99' }, conflictingProperties: ['age'], baseVersion: 1 })
];

const result = getWithConflictDetails(defs);
const audits = [{ details: { entityDefId: 0 } }];

const result = getWithConflictDetails(defs, audits, false);

result[2].conflict.should.be.eql(ConflictType.HARD);
result[2].baseDiff.should.be.eql(['age']);
Expand Down

0 comments on commit 7787851

Please sign in to comment.