Skip to content

Commit

Permalink
Remove submissionCreate from entity audit events
Browse files Browse the repository at this point in the history
  • Loading branch information
ktuite committed Jan 18, 2024
1 parent 983ec81 commit 32086c5
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 73 deletions.
2 changes: 1 addition & 1 deletion docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13436,7 +13436,7 @@ components:
source:
type: object
description: The source of the Entity version, such as a Submission or an API request. This property is experimental and may change in the future.
example: {event: {}, submissionCreate: {}, submission: {}}
example: {event: {}, submission: {}}
baseDiff:
type: array
description: List of properties that are different between this version and its base version. Includes the label if it differs between the two versions.
Expand Down
4 changes: 2 additions & 2 deletions lib/data/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,15 +365,15 @@ const getWithConflictDetails = (defs, audits, relevantToConflict) => {
const relevantBaseVersions = new Set();

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

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

Expand Down
32 changes: 21 additions & 11 deletions lib/model/query/audits.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,20 +192,30 @@ const getByEntityId = (entityId, options) => ({ all }) => {
.map(a => a.withAux('actor', audit.aux.triggeringEventActor.orNull()))
.map(a => a.forApi());

const submissionCreate = audit.aux.submissionCreateEvent
.map(a => a.withAux('actor', audit.aux.submissionCreateEventActor.orNull()))
.map(a => a.forApi());

const submission = audit.aux.submission
.map(s => s.withAux('submitter', audit.aux.submissionActor.orNull()))
.map(s => s.withAux('currentVersion', audit.aux.currentVersion.map(v => v.withAux('submitter', audit.aux.currentSubmissionActor.orNull()))))
.map(s => s.forApi())
.map(s => mergeLeft(s, { xmlFormId: audit.aux.form.map(f => f.xmlFormId).orNull() }));
// if entity event is based on a submission, get the rest of the details from the submission create
// event even if submission is deleted.
// otherwise, leave submission and don't include in audit details.
let submission;
const createEvent = audit.aux.submissionCreateEvent.orNull();
if (createEvent) {
const baseSubmission = {
instanceId: createEvent.details.instanceId,
createdAt: createEvent.loggedAt,
actor: audit.aux.submissionCreateEventActor.get().forApi()
};

const fullSubmission = audit.aux.submission
.map(s => s.withAux('submitter', audit.aux.submissionActor.orNull()))
.map(s => s.withAux('currentVersion', audit.aux.currentVersion.map(v => v.withAux('submitter', audit.aux.currentSubmissionActor.orNull()))))
.map(s => s.forApi())
.map(s => mergeLeft(s, { xmlFormId: audit.aux.form.map(f => f.xmlFormId).orNull() }));

submission = mergeLeft(baseSubmission, fullSubmission.orElse(undefined));
}

const details = mergeLeft(audit.details, {
sourceEvent: sourceEvent.orElse(undefined),
submissionCreate: submissionCreate.orElse(undefined),
submission: submission.orElse(undefined)
submission
});

return new Audit({ ...audit, details }, { actor: audit.aux.actor });
Expand Down
227 changes: 168 additions & 59 deletions test/integration/api/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -923,86 +923,195 @@ describe('Entities API', () => {
});
}));

it('should return instanceId even when submission is deleted', testEntities(async (service, container) => {
const asAlice = await service.login('alice');
describe('entity source within an audit event', () => {
it('should not include submission or source event when entity created or updated via API', testService(async (service) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);
await asAlice.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.simpleEntity)
.expect(200);

await container.Forms.purge(true);
await asAlice.post('/v1/projects/1/datasets/people/entities')
.send({
uuid: '12345678-1234-4123-8234-123456789abc',
label: 'Johnny Doe',
data: { first_name: 'Johnny', age: '22' }
})
.expect(200);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.should.not.have.property('submission');
logs[0].details.should.not.have.property('sourceEvent');
});

logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');
await asAlice.patch('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc?force=true')
.send({ label: 'two' })
.expect(200);

logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.loggedAt.should.be.isoDate();
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.should.not.have.property('submission');
logs[0].details.should.not.have.property('sourceEvent');
});
}));

logs[0].details.should.not.have.property('submission');
it('should return source when entity created via submission approval', testEntities(async (service) => {
const asAlice = await service.login('alice');

logs[0].details.submissionCreate.details.instanceId.should.be.eql('one');
logs[0].details.submissionCreate.actor.displayName.should.be.eql('Alice');
logs[0].details.submissionCreate.loggedAt.should.be.isoDate();
});
}));
// testEntities creates an entity on submission approval
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.submission.should.be.a.Submission();
logs[0].details.submission.instanceId.should.be.eql('one');
logs[0].details.submission.xmlFormId.should.be.eql('simpleEntity');
logs[0].details.submission.currentVersion.instanceName.should.be.eql('one');

it('should return instanceId even when form is deleted', testEntities(async (service) => {
const asAlice = await service.login('alice');
logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.action.should.be.eql('submission.update');
});
}));

await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);
it('should return source when entity created via submission creation', testService(async (service, container) => {
const asAlice = await service.login('alice');

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
await asAlice.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.simpleEntity)
.expect(200);

logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');
await asAlice.post('/v1/projects/1/forms/simpleEntity/submissions')
.send(testData.instances.simpleEntity.one)
.set('Content-Type', 'application/xml')
.expect(200);

logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.loggedAt.should.be.isoDate();
await exhaust(container);

logs[0].details.should.not.have.property('submission');
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.submission.should.be.a.Submission();
logs[0].details.submission.instanceId.should.be.eql('one');
logs[0].details.submission.xmlFormId.should.be.eql('simpleEntity');
logs[0].details.submission.currentVersion.instanceName.should.be.eql('one');

logs[0].details.submissionCreate.details.instanceId.should.be.eql('one');
logs[0].details.submissionCreate.actor.displayName.should.be.eql('Alice');
logs[0].details.submissionCreate.loggedAt.should.be.isoDate();
});
}));
logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.action.should.be.eql('submission.create');
});
}));

// It's not possible to purge audit logs via API.
// However System Administrators can purge/archive audit logs via SQL
// to save disk space and improve performance
it('should return entity audits even when submission and its logs are deleted', testEntities(async (service, container) => {
const asAlice = await service.login('alice');
it('should return source when entity updated via submission', testEntityUpdates(async (service, container) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);
// testEntityUpdates does the following: creates dataset, creates update form. test needs to submit update.
await asAlice.post('/v1/projects/1/forms/updateEntity/submissions')
.send(testData.instances.updateEntity.one)
.set('Content-Type', 'application/xml')
.expect(200);

await container.Forms.purge(true);
await exhaust(container);

await container.run(sql`DELETE FROM audits WHERE action like 'submission%'`);
await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.submission.should.be.a.Submission();
logs[0].details.submission.instanceId.should.be.eql('one');
logs[0].details.submission.xmlFormId.should.be.eql('updateEntity');
logs[0].details.submission.currentVersion.instanceName.should.be.eql('one');

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.action.should.be.eql('submission.create');
});
}));

logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');
it('should return instanceId even when submission is deleted', testEntities(async (service, container) => {
const asAlice = await service.login('alice');

logs[0].details.should.not.have.property('approval');
logs[0].details.should.not.have.property('submission');
logs[0].details.should.not.have.property('submissionCreate');
});
}));
await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);

await container.Forms.purge(true);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');

logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.loggedAt.should.be.isoDate();

logs[0].details.submission.instanceId.should.be.eql('one');
logs[0].details.submission.actor.displayName.should.be.eql('Alice');
logs[0].details.submission.createdAt.should.be.isoDate();

// submission is only a stub so it doesn't have things like instanceName or currentVersion
logs[0].details.submission.should.not.have.property('instanceName');
logs[0].details.submission.should.not.have.property('currentVersion');
});
}));

it('should return instanceId even when form is deleted', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {
logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');

logs[0].details.sourceEvent.should.be.an.Audit();
logs[0].details.sourceEvent.actor.displayName.should.be.eql('Alice');
logs[0].details.sourceEvent.loggedAt.should.be.isoDate();

logs[0].details.submission.instanceId.should.be.eql('one');
logs[0].details.submission.actor.displayName.should.be.eql('Alice');
logs[0].details.submission.createdAt.should.be.isoDate();

// submission is only a stub so it doesn't have things like instanceName or currentVersion
logs[0].details.submission.should.not.have.property('instanceName');
logs[0].details.submission.should.not.have.property('currentVersion');
});
}));

// It's not possible to purge audit logs via API.
// However System Administrators can purge/archive audit logs via SQL
// to save disk space and improve performance
it('should return entity audits even when submission and its logs are deleted', testEntities(async (service, container) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/forms/simpleEntity')
.expect(200);

await container.Forms.purge(true);

await container.run(sql`DELETE FROM audits WHERE action like 'submission%'`);

await asAlice.get('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc/audits')
.expect(200)
.then(({ body: logs }) => {

logs[0].should.be.an.Audit();
logs[0].action.should.be.eql('entity.create');
logs[0].actor.displayName.should.be.eql('Alice');

logs[0].details.should.not.have.property('approval');
logs[0].details.should.not.have.property('submission');
logs[0].details.should.not.have.property('submissionCreate');
});
}));
});

it('should return right approval details when we have multiple approvals', testService(async (service, container) => {
const asAlice = await service.login('alice');
Expand Down

0 comments on commit 32086c5

Please sign in to comment.