diff --git a/lib/model/query/entities.js b/lib/model/query/entities.js index 04943fb69..b8aeff15e 100644 --- a/lib/model/query/entities.js +++ b/lib/model/query/entities.js @@ -237,8 +237,19 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss const clientEntity = await Entity.fromParseEntityData(entityData, { update: true }); // validation happens here // Get version of entity on the server - const serverEntity = (await Entities.getById(dataset.id, clientEntity.uuid, QueryOptions.forUpdate)) - .orThrow(Problem.user.entityNotFound({ entityUuid: clientEntity.uuid, datasetName: dataset.name })); + // TODO: comments about what is happening here, too + let serverEntity = await Entities.getById(dataset.id, clientEntity.uuid, QueryOptions.forUpdate); + if (!serverEntity.isDefined()) { + if (clientEntity.def.branchId == null) { + throw Problem.user.entityNotFound({ entityUuid: clientEntity.uuid, datasetName: dataset.name }); + } else { + await _holdSubmission(run, submissionDef.submissionId, submissionDef.id, clientEntity.def.branchId, clientEntity.def.baseVersion); + return null; + } + } else { + serverEntity = serverEntity.get(); + } + let { conflict } = serverEntity; let conflictingProperties; // Maybe we don't need to persist this??? just compute at the read time @@ -391,7 +402,7 @@ const _processSubmissionEvent = (event, parentEvent) => async ({ Audits, Dataset // Check for held submissions that follow this one in the same branch if (entityData.system.branchId != null) { // baseVersion could be '', meaning its a create - const currentBaseVersion = entityData.system.baseVersion === '' ? 1 : parseInt(entityData.system.baseVersion, 10); + const currentBaseVersion = entityData.system.baseVersion === '' ? 0 : parseInt(entityData.system.baseVersion, 10); const nextSub = await _checkHeldSubmission(maybeOne, entityData.system.branchId, currentBaseVersion + 1); if (nextSub.isDefined()) { const { submissionId: nextSubmissionId, submissionDefId: nextSubmissionDefId } = nextSub.get(); diff --git a/test/integration/api/offline-entities.js b/test/integration/api/offline-entities.js index d72f6b56a..58fd19278 100644 --- a/test/integration/api/offline-entities.js +++ b/test/integration/api/offline-entities.js @@ -202,6 +202,40 @@ describe('Offline Entities', () => { entity.aux.currentVersion.branchBaseVersion.should.equal(2); entity.aux.currentVersion.data.should.eql({ age: '22', status: 'departed', first_name: 'Johnny' }); })); + + it('should handle an offline branch that starts with a create', testOfflineEntities(async (service, container) => { + const asAlice = await service.login('alice'); + const branchId = uuid(); + const dataset = await container.Datasets.get(1, 'people', true).then(getOrNotFound); + + // First submission creates the entity, offline version is now 1 + await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions') + .send(testData.instances.offlineEntity.two + .replace('branchId=""', `branchId="${branchId}"`) + ) + .set('Content-Type', 'application/xml') + .expect(200); + + // Second submission updates the entity + await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions') + .send(testData.instances.offlineEntity.two + .replace('create="1"', 'update="1"') + .replace('branchId=""', `branchId="${branchId}"`) + .replace('two', 'two-update') + .replace('baseVersion=""', 'baseVersion="1"') + .replace('new', 'checked in') + ) + .set('Content-Type', 'application/xml') + .expect(200); + + await exhaust(container); + + const entity = await container.Entities.getById(dataset.id, '12345678-1234-4123-8234-123456789ddd').then(getOrNotFound); + entity.aux.currentVersion.branchId.should.equal(branchId); + entity.aux.currentVersion.version.should.equal(2); + entity.aux.currentVersion.baseVersion.should.equal(1); + entity.aux.currentVersion.data.should.eql({ age: '20', status: 'checked in', first_name: 'Megan' }); + })); }); describe('out of order runs', () => { @@ -337,5 +371,44 @@ describe('Offline Entities', () => { entity.aux.currentVersion.baseVersion.should.equal(3); entity.aux.currentVersion.data.should.eql({ age: '22', status: 'departed', first_name: 'Johnny' }); })); + + it('should handle offline update that comes before a create', testOfflineEntities(async (service, container) => { + const asAlice = await service.login('alice'); + const branchId = uuid(); + const dataset = await container.Datasets.get(1, 'people', true).then(getOrNotFound); + + // Second submission updates the entity but entity hasn't been created yet + await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions') + .send(testData.instances.offlineEntity.two + .replace('create="1"', 'update="1"') + .replace('branchId=""', `branchId="${branchId}"`) + .replace('two', 'two-update') + .replace('baseVersion=""', 'baseVersion="1"') + .replace('new', 'checked in') + ) + .set('Content-Type', 'application/xml') + .expect(200); + + await exhaust(container); + + const backlogCount = await container.oneFirst(sql`select count(*) from entity_submission_backlog`); + backlogCount.should.equal(1); + + // First submission creating the entity comes in later + await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions') + .send(testData.instances.offlineEntity.two + .replace('branchId=""', `branchId="${branchId}"`) + ) + .set('Content-Type', 'application/xml') + .expect(200); + + await exhaust(container); + + const entity = await container.Entities.getById(dataset.id, '12345678-1234-4123-8234-123456789ddd').then(getOrNotFound); + entity.aux.currentVersion.branchId.should.equal(branchId); + entity.aux.currentVersion.version.should.equal(2); + entity.aux.currentVersion.baseVersion.should.equal(1); + entity.aux.currentVersion.data.should.eql({ age: '20', status: 'checked in', first_name: 'Megan' }); + })); }); });