Skip to content

Commit

Permalink
entity submission create-as-update
Browse files Browse the repository at this point in the history
  • Loading branch information
ktuite committed Oct 23, 2024
1 parent e2f3039 commit c65ea25
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 23 deletions.
52 changes: 44 additions & 8 deletions lib/model/query/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ const _createEntity = (dataset, entityData, submissionId, submissionDef, submiss
? _partial.auxWith('def', { branchBaseVersion: _partial.def.baseVersion })
: _partial;

// Because of entity processing flow, we need to check for a conflict explicitly without
// distrupting the database transaction.
const maybeEntity = await Entities.getById(dataset.id, partial.uuid);
if (maybeEntity.isDefined())
throw Problem.user.uniquenessViolation({ fields: ['uuid'], values: partial.uuid });


const sourceDetails = {
submission: { instanceId: submissionDef.instanceId },
parentEventId: parentEvent ? parentEvent.id : undefined,
Expand All @@ -252,14 +259,14 @@ const _createEntity = (dataset, entityData, submissionId, submissionDef, submiss
return entity;
};

const _updateEntity = (dataset, entityData, submissionId, submissionDef, submissionDefId, event, forceOutOfOrderProcessing = false) => async ({ Audits, Entities }) => {
const _updateEntity = (dataset, entityData, submissionId, submissionDef, submissionDefId, event, forceOutOfOrderProcessing = false, createAsUpdate = false) => async ({ Audits, Entities }) => {
if (!(event.action === 'submission.create'
|| event.action === 'submission.update.version'
|| event.action === 'submission.reprocess'))
return null;

// Get client version of entity
const clientEntity = await Entity.fromParseEntityData(entityData, { update: true }); // validation happens here
const clientEntity = await Entity.fromParseEntityData(entityData, createAsUpdate ? { create: true } : { update: true }); // validation happens here

// Figure out the intended baseVersion
// If this is an offline update with a branchId, the baseVersion value is local to that offline context.
Expand All @@ -268,7 +275,7 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss
// Try computing base version.
// But if there is a 404.8 not found error, double-check if the entity never existed or was deleted.
try {
baseEntityDef = await Entities._computeBaseVersion(event.id, dataset, clientEntity, submissionDef, forceOutOfOrderProcessing);
baseEntityDef = await Entities._computeBaseVersion(event.id, dataset, clientEntity, submissionDef, forceOutOfOrderProcessing, createAsUpdate);
} catch (err) {
if (err.problemCode === 404.8) {
// Look up deleted entity by passing deleted as option argData
Expand Down Expand Up @@ -315,6 +322,8 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss
if (conflict !== ConflictType.HARD) { // We don't want to downgrade conflict here
conflict = conflictingProperties.length > 0 ? ConflictType.HARD : ConflictType.SOFT;
}
} else if (createAsUpdate) {
conflict = ConflictType.SOFT;
}

// merge data
Expand All @@ -324,7 +333,8 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss
// make some kind of source object
const sourceDetails = {
submission: { instanceId: submissionDef.instanceId },
forceProcessed: forceOutOfOrderProcessing
forceProcessed: forceOutOfOrderProcessing,
createAsUpdate
};
const sourceId = await Entities.createSource(sourceDetails, submissionDefId, event.id);
const partial = new Entity.Partial(serverEntity.with({ conflict }), {
Expand Down Expand Up @@ -359,8 +369,16 @@ const _updateEntity = (dataset, entityData, submissionId, submissionDef, submiss

// Used by _updateVerison to figure out the intended base version in Central
// based on the branchId, trunkVersion, and baseVersion in the submission
const _computeBaseVersion = (eventId, dataset, clientEntity, submissionDef, forceOutOfOrderProcessing = false) => async ({ Entities }) => {
if (!clientEntity.def.branchId) {
const _computeBaseVersion = (eventId, dataset, clientEntity, submissionDef, forceOutOfOrderProcessing = false, createAsUpdate = false) => async ({ Entities }) => {
if (clientEntity.def.baseVersion == null && createAsUpdate) {
// if no baseVersion is specified but we are updating and trying to find the base version
// we are probably in the special case of force-apply create-as-update. get the latest version.

const latestEntity = await Entities.getById(dataset.id, clientEntity.uuid)
.then(getOrReject(Problem.user.entityNotFound({ entityUuid: clientEntity.uuid, datasetName: dataset.name })));
return latestEntity.aux.currentVersion;

} else if (!clientEntity.def.branchId) {

// no offline branching to deal with, use baseVersion as is
const condition = { version: clientEntity.def.baseVersion };
Expand Down Expand Up @@ -504,8 +522,26 @@ const _processSubmissionEvent = (event, parentEvent) => async ({ Audits, Dataset
throw (err);
}
}
else if (entityData.system.create === '1' || entityData.system.create === 'true')
maybeEntity = await Entities._createEntity(dataset, entityData, submissionId, submissionDef, submissionDefId, event, parentEvent, forceOutOfOrderProcessing);
else if (entityData.system.create === '1' || entityData.system.create === 'true') {
// note i dont think (???) forceOutOfOrderProcessing will ever be true here?
try {
maybeEntity = await Entities._createEntity(dataset, entityData, submissionId, submissionDef, submissionDefId, event, parentEvent, forceOutOfOrderProcessing);
} catch (err) {
// There was a problem creating the entity
// If it is a uuid collision, check if the entity was created via an update
// in which case its ok to apply this create as an update
if (err.problemCode === 409.3) {
const rootDef = await Entities.getDef(dataset.id, entityData.system.id, new QueryOptions({ root: true })).then(o => o.orNull());
if (rootDef && rootDef.aux.source.details.updateAsCreate && rootDef.aux.source.details.forceProcessed) {
maybeEntity = await Entities._updateEntity(dataset, entityData, submissionId, submissionDef, submissionDefId, event, forceOutOfOrderProcessing, true);
} else {
throw (err);
}
} else {
throw (err);
}
}
}

// Check for held submissions that follow this one in the same branch
if (maybeEntity != null) {
Expand Down
57 changes: 42 additions & 15 deletions test/integration/api/offline-entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ describe('Offline Entities', () => {
backlogCount.should.equal(0);
}));

it('should apply an entity update as a create', testOfflineEntities(async (service, container) => {
it('should apply an entity update as a create [update-as-create]', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();
const newUuid = uuid();
Expand Down Expand Up @@ -1003,7 +1003,7 @@ describe('Offline Entities', () => {
backlogCount.should.equal(0);
}));

it('should apply an entity update as a create followed by another update', testOfflineEntities(async (service, container) => {
it('should apply an entity update as a create followed by another update [update-as-create]', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();
const newUuid = uuid();
Expand Down Expand Up @@ -1091,7 +1091,7 @@ describe('Offline Entities', () => {
backlogCount.should.equal(0);
}));

it.skip('should apply an entity update as a create, and then properly handle the delayed create', testOfflineEntities(async (service, container) => {
it('should apply an entity update as a create, and then properly handle the delayed create [create-as-update]', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();

Expand Down Expand Up @@ -1126,8 +1126,8 @@ describe('Offline Entities', () => {
body.currentVersion.data.should.eql({ status: 'checked in' });
body.currentVersion.label.should.eql('auto generated');
body.currentVersion.branchId.should.equal(branchId);
body.currentVersion.branchBaseVersion.should.equal(1);
should.not.exist(body.currentVersion.baseVersion);
should.not.exist(body.currentVersion.branchBaseVersion); // No base version because this is a create, though maybe this should be here.
should.not.exist(body.currentVersion.trunkVersion);
});

Expand All @@ -1136,25 +1136,16 @@ describe('Offline Entities', () => {

// First submission creates the entity, but this will be processed as an update
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.two
.replace('branchId=""', `branchId="${branchId}"`)
)
.send(testData.instances.offlineEntity.two)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);

// In the default behavior, attempting create on an entity that already exists causes a conflict error.
await asAlice.get('/v1/projects/1/forms/offlineEntity/submissions/two/audits')
.expect(200)
.then(({ body }) => {
body[0].details.errorMessage.should.eql('A resource already exists with uuid value(s) of 12345678-1234-4123-8234-123456789ddd.');
});

await asAlice.get(`/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd`)
.expect(200)
.then(({ body }) => {
body.currentVersion.version.should.equal(1);
body.currentVersion.version.should.equal(2);
});
}));

Expand Down Expand Up @@ -1434,6 +1425,42 @@ describe('Offline Entities', () => {
});
}));

it('should mark a create-as-update', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');
const branchId = uuid();

// Send update first
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"')
)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);

// Force the update submission to be processed as a create
await container.Entities.processBacklog(true);

// Send create in next to be applied as an update
await asAlice.post('/v1/projects/1/forms/offlineEntity/submissions')
.send(testData.instances.offlineEntity.two)
.set('Content-Type', 'application/xml')
.expect(200);

await exhaust(container);

await asAlice.get(`/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789ddd/versions`)
.expect(200)
.then(({ body }) => {
body[1].source.createAsUpdate.should.equal(true);
body[1].source.forceProcessed.should.equal(false);
});
}));

it('should mark a regular update as not force processed', testOfflineEntities(async (service, container) => {
const asAlice = await service.login('alice');

Expand Down

0 comments on commit c65ea25

Please sign in to comment.