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' });
+ }));
});
});