Skip to content

Commit 31ce9ce

Browse files
committed
wip: soft-delete draft submissions when drafts get replaced
1 parent afa0397 commit 31ce9ce

File tree

6 files changed

+82
-6
lines changed

6 files changed

+82
-6
lines changed

lib/model/query/forms.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,20 @@ const _draftFilter = (form, project) =>
485485
? sql`and forms."projectId" = ${project.id}`
486486
: sql``));
487487

488-
// NOTE: copypasta alert! The following SQL also appears in 20220209-01-purge-unneeded-drafts.js
488+
// NOTE: copypasta alert! Similar SQL also appears in 20220209-01-purge-unneeded-drafts.js
489+
// Purges draft form defs that are not referenced by any forms AND have no associated submission defs.
489490
const clearUnneededDrafts = (form = null, project = null) => ({ run }) =>
490491
run(sql`
491492
DELETE FROM form_defs
492493
USING forms
493494
WHERE form_defs."formId" = forms.id
494495
AND form_defs."publishedAt" IS NULL
495496
AND form_defs.id IS DISTINCT FROM forms."draftDefId"
497+
AND NOT EXISTS (
498+
SELECT 1
499+
FROM submission_defs
500+
WHERE submission_defs."formDefId" = form_defs.id
501+
)
496502
${_draftFilter(form, project)}`)
497503
.then(() => run(sql`
498504
DELETE FROM form_schemas

lib/model/query/submissions.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ const clearDraftSubmissions = (formId) => ({ run }) =>
121121
const clearDraftSubmissionsForProject = (projectId) => ({ run }) =>
122122
run(sql`DELETE FROM submissions USING forms WHERE submissions."formId" = forms.id AND forms."projectId" = ${projectId} AND submissions.draft=true`);
123123

124+
const softDeleteDraftSubmissions = (formId) => ({ run }) =>
125+
run(sql`UPDATE submissions SET "deletedAt"=now() WHERE "formId"=${formId} AND "draft"=true AND "deletedAt" IS NULL`);
126+
124127
////////////////////////////////////////////////////////////////////////////////
125128
// SELECT-MULTIPLE VALUES
126129

@@ -475,7 +478,7 @@ select count(*) from deleted_submissions`);
475478

476479
module.exports = {
477480
createNew, createVersion,
478-
update, del, restore, purge, clearDraftSubmissions, clearDraftSubmissionsForProject,
481+
update, del, restore, purge, clearDraftSubmissions, clearDraftSubmissionsForProject, softDeleteDraftSubmissions,
479482
setSelectMultipleValues, getSelectMultipleValuesForExport,
480483
getByIdsWithDef, getSubAndDefById,
481484
getByIds, getAllForFormByIds, getById, countByFormId, verifyVersion,

lib/resources/forms.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ module.exports = (service, endpoint) => {
137137
: getPartial(Forms, request, project, Keys)))
138138
.then((partial) => Promise.all([
139139
Forms.createVersion(partial, form, false),
140-
Submissions.clearDraftSubmissions(form.id)
140+
Submissions.softDeleteDraftSubmissions(form.id)
141141
]))
142142
.then(() => Forms.clearUnneededDrafts(form))) // remove drafts made obsolete by new draft
143143
.then(success)));
@@ -162,7 +162,7 @@ module.exports = (service, endpoint) => {
162162
.then(() => Forms.getByProjectAndXmlFormId(params.projectId, params.id, false, Form.DraftVersion))
163163
.then(getOrNotFound)
164164
: resolve(form)))
165-
.then(((form) => Promise.all([ Forms.publish(form), Submissions.clearDraftSubmissions(form.id) ])))
165+
.then(((form) => Promise.all([ Forms.publish(form), Submissions.softDeleteDraftSubmissions(form.id) ])))
166166
.then(success)));
167167

168168
// Entity/Dataset-specific endpoint that is used to show how publishing
@@ -244,7 +244,7 @@ module.exports = (service, endpoint) => {
244244
.then(rejectIf(((form) => form.draftDefId == null), noargs(Problem.user.notFound)))
245245
.then((form) => Promise.all([
246246
Forms.clearDraft(form).then(() => Forms.clearUnneededDrafts(form)),
247-
Submissions.clearDraftSubmissions(form.id),
247+
Submissions.softDeleteDraftSubmissions(form.id),
248248
Audits.log(auth.actor, 'form.update.draft.delete', form, { oldDraftDefId: form.draftDefId })
249249
]))
250250
.then(success)));

lib/task/purge.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ const purgeTask = task.withContainer((container) => async (options = {}) => {
2121
const count = await Forms.purge(options.force, options.formId, options.projectId, options.xmlFormId);
2222
return `Forms purged: ${count}`;
2323
} else {
24+
// Purge both Forms and Submissions according to options
2425
const formCount = await Forms.purge(options.force, options.formId, options.projectId, options.xmlFormId);
2526
const submissionCount = await Submissions.purge(options.force, options.projectId, options.xmlFormId, options.instanceId);
27+
28+
// Related to form purging: deletes draft form defs that are not in use by any form and have no associated submission defs
29+
await Forms.clearUnneededDrafts();
30+
2631
return `Forms purged: ${formCount}, Submissions purged: ${submissionCount}`;
2732
}
2833
} catch (error) {

test/integration/api/forms/draft.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,68 @@ describe('api: /projects/:id/forms (drafts)', () => {
844844
.then((counts) => counts.should.eql([ 1, 4, 3 ])))));
845845
});
846846
});
847+
848+
describe('preserving submissions from old or deleted drafts', () => {
849+
it('should soft-delete submissions of undeeded draft when a new version is uploaded', testService(async (service, { oneFirst }) => {
850+
const asAlice = await service.login('alice');
851+
852+
await asAlice.post('/v1/projects/1/forms/simple/draft')
853+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty"'))
854+
.set('Content-Type', 'application/xml')
855+
.expect(200);
856+
857+
await asAlice.post('/v1/projects/1/forms/simple/draft/submissions')
858+
.send(testData.instances.simple.one)
859+
.set('Content-Type', 'text/xml')
860+
.expect(200);
861+
862+
let subs = await oneFirst(sql`select count(*) from submissions`);
863+
subs.should.equal(1);
864+
865+
await asAlice.post('/v1/projects/1/forms/simple/draft')
866+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty2"'))
867+
.set('Content-Type', 'application/xml')
868+
.expect(200);
869+
870+
subs = await oneFirst(sql`select count(*) from submissions`);
871+
subs.should.equal(1);
872+
873+
const fds = await oneFirst(sql`select count(*) from form_defs as fd join forms as f on fd."formId" = f.id where f."xmlFormId"='simple'`);
874+
fds.should.equal(3); // Old draft has not been deleted. Count also includes published and new draft.
875+
}));
876+
});
877+
878+
it('should purge old draft submissions after 30 days', testService(async (service, { oneFirst, run, Forms, Submissions }) => {
879+
const asAlice = await service.login('alice');
880+
881+
await asAlice.post('/v1/projects/1/forms/simple/draft')
882+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty"'))
883+
.set('Content-Type', 'application/xml')
884+
.expect(200);
885+
886+
await asAlice.post('/v1/projects/1/forms/simple/draft/submissions')
887+
.send(testData.instances.simple.one)
888+
.set('Content-Type', 'text/xml')
889+
.expect(200);
890+
891+
let subs = await oneFirst(sql`select count(*) from submissions`);
892+
subs.should.equal(1);
893+
894+
await asAlice.post('/v1/projects/1/forms/simple/draft')
895+
.send(testData.forms.simple.replace('id="simple"', 'id="simple" version="drafty2"'))
896+
.set('Content-Type', 'application/xml')
897+
.expect(200);
898+
899+
await run(sql`update submissions set "deletedAt" = '1999-1-1T00:00:00Z' where "deletedAt" is not null`);
900+
await Submissions.purge();
901+
await Forms.clearUnneededDrafts();
902+
903+
subs = await oneFirst(sql`select count(*) from submissions`);
904+
subs.should.equal(0);
905+
906+
const fds = await oneFirst(sql`select count(*) from form_defs as fd join forms as f on fd."formId" = f.id where f."xmlFormId"='simple'`);
907+
fds.should.equal(2); // Old draft has now been deleted. Count also includes published and new draft.
908+
}));
847909
});
848910
});
849911

test/integration/api/submissions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3294,7 +3294,7 @@ one,h,/data/h,2000-01-01T00:06,2000-01-01T00:07,-5,-6,,ee,ff
32943294
.expect(200)
32953295
.then(({ body }) => { body.should.eql([]); })))));
32963296

3297-
it('should not carry over draft keys when a draft is replaced', testService((service) =>
3297+
it.only('should not carry over draft keys when a draft is replaced', testService((service) =>
32983298
service.login('alice', (asAlice) =>
32993299
asAlice.post('/v1/projects/1/forms?publish=true')
33003300
.send(testData.forms.encrypted)

0 commit comments

Comments
 (0)