Skip to content

Commit

Permalink
Add xmlFormId to form purge
Browse files Browse the repository at this point in the history
  • Loading branch information
ktuite committed Sep 15, 2023
1 parent a46f05c commit 636b3b3
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 25 deletions.
2 changes: 1 addition & 1 deletion lib/bin/create-docker-databases.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const connect = (database) => knex({
connection: { host: 'localhost', user: 'postgres', password: 'odktest', database }
});

program.option('-l', 'Print all db statements to log.');
program.option('-l, --log', 'Print all db statements to log.');
program.parse();
const { log } = program.opts();

Expand Down
9 changes: 5 additions & 4 deletions lib/bin/purge-forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ const { run } = require('../task/task');
const { purgeForms } = require('../task/purge');

const { program } = require('commander');
program.option('-f', 'Force any soft-deleted form to be purged right away.');
program.option('-i <integer>', 'Purge a specific form based on its id.', parseInt);
program.option('-p <integer>', 'Restrict purging to a specific project.', parseInt);
program.option('-f, --force', 'Force any soft-deleted form to be purged right away.');
program.option('-i, --formId <integer>', 'Purge a specific form based on its id.', parseInt);
program.option('-p, --projectId <integer>', 'Restrict purging to a specific project.', parseInt);
program.option('-x, --xmlFormId <value>', 'Restrict purging to specific form based on xmlFormId (must be used with project id).');
program.parse();

const options = program.opts();

run(purgeForms(options.force, options.formId, options.projectId)
run(purgeForms(options.force, options.formId, options.projectId, options.xmlFormId)
.then((count) => `Forms purged: ${count}`));
24 changes: 15 additions & 9 deletions lib/model/query/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,16 +309,19 @@ const DAY_RANGE = config.has('default.taskSchedule.purge')
? config.get('default.taskSchedule.purge')
: 30; // Default is 30 days

const _trashedFilter = (force, id, projectId) => {
const _trashedFilter = (force, id, projectId, xmlFormId) => {
const idFilter = (id
? sql`and forms.id = ${id}`
: sql``);
const projectFilter = (projectId
? sql`and forms."projectId" = ${projectId}`
: sql``);
const xmlFormIdFilter = ((xmlFormId && projectId)
? sql`and forms."projectId" = ${projectId} and forms."xmlFormId" = ${xmlFormId}`
: sql``);
return (force
? sql`forms."deletedAt" is not null ${idFilter} ${projectFilter}`
: sql`forms."deletedAt" < current_date - cast(${DAY_RANGE} as int) ${idFilter} ${projectFilter}`);
? sql`forms."deletedAt" is not null ${idFilter} ${projectFilter} ${xmlFormIdFilter}`
: sql`forms."deletedAt" < current_date - cast(${DAY_RANGE} as int) ${idFilter} ${projectFilter} ${xmlFormIdFilter}`);
};

// NOTE: copypasta alert!
Expand All @@ -333,18 +336,20 @@ const _trashedFilter = (force, id, projectId) => {
// 3. Update actees table for the specific form to leave some useful information behind
// 4. Delete the forms and their resources from the database
// 5. Purge unattached blobs
const purge = (force = false, id = null, projectId = null) => ({ oneFirst, Blobs }) =>
oneFirst(sql`
const purge = (force = false, id = null, projectId = null, xmlFormId = null) => ({ oneFirst, Blobs }) => {
if (xmlFormId != null && projectId == null)
throw Problem.internal.unknown({ error: 'Must also specify projectId when using xmlFormId' });
return oneFirst(sql`
with redacted_audits as (
update audits set notes = ''
from forms
where audits."acteeId" = forms."acteeId"
and ${_trashedFilter(force, id, projectId)}
and ${_trashedFilter(force, id, projectId, xmlFormId)}
), purge_audits as (
insert into audits ("action", "acteeId", "loggedAt", "processed")
select 'form.purge', "acteeId", clock_timestamp(), clock_timestamp()
from forms
where ${_trashedFilter(force, id, projectId)}
where ${_trashedFilter(force, id, projectId, xmlFormId)}
), update_actees as (
update actees set "purgedAt" = clock_timestamp(),
"purgedName" = form_defs."name",
Expand All @@ -356,15 +361,16 @@ with redacted_audits as (
from forms
left outer join form_defs on coalesce(forms."currentDefId", forms."draftDefId") = form_defs.id
where actees.id = forms."acteeId"
and ${_trashedFilter(force, id, projectId)}
and ${_trashedFilter(force, id, projectId, xmlFormId)}
), deleted_forms as (
delete from forms
where ${_trashedFilter(force, id, projectId)}
where ${_trashedFilter(force, id, projectId, xmlFormId)}
returning *
)
select count(*) from deleted_forms`)
.then((count) => Blobs.purgeUnattached()
.then(() => Promise.resolve(count)));
};

////////////////////////////////////////////////////////////////////////////////
// CLEARING UNNEEDED DRAFTS
Expand Down
4 changes: 2 additions & 2 deletions lib/task/purge.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

const { task } = require('./task');

const purgeForms = task.withContainer(({ Forms }) => (force = false, formId = null, projectId = null) =>
Forms.purge(force, formId, projectId));
const purgeForms = task.withContainer(({ Forms }) => (force = false, formId = null, projectId = null, xmlFormId = null) =>
Forms.purge(force, formId, projectId, xmlFormId));

module.exports = { purgeForms };
104 changes: 95 additions & 9 deletions test/integration/task/purge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const appRoot = require('app-root-path');
const { testTask } = require('../setup');
const assert = require('assert');
const { testTask, testService } = require('../setup');
const { purgeForms } = require(appRoot + '/lib/task/purge');
const testData = require('../../data/xml');

// The basics of this task are tested here, including returning the count
// eslint-disable-next-line no-trailing-spaces
Expand All @@ -26,14 +28,13 @@ describe('task: purge deleted forms', () => {

it('should return count for multiple forms purged', testTask(({ Forms }) =>
Forms.getByProjectAndXmlFormId(1, 'simple')
.then((form) => Forms.del(form.get())
.then(() => Forms.getByProjectAndXmlFormId(1, 'withrepeat'))
// eslint-disable-next-line no-shadow
.then((form) => Forms.del(form.get())
.then(() => purgeForms(true))
.then((count) => {
count.should.equal(2);
})))));
.then((form) => Forms.del(form.get()))
.then(() => Forms.getByProjectAndXmlFormId(1, 'withrepeat')
.then((form) => Forms.del(form.get())))
.then(() => purgeForms(true)
.then((count) => {
count.should.equal(2);
}))));

it('should not purge specific recently deleted form', testTask(({ Forms }) =>
Forms.getByProjectAndXmlFormId(1, 'simple')
Expand Down Expand Up @@ -123,5 +124,90 @@ describe('task: purge deleted forms', () => {
count.should.equal(0);
})))));
});

describe('with xmlFormId', () => {
it('should thow error if xmlFormId specified without projectId', testTask(async ({ Forms }) => {
const form = await Forms.getByProjectAndXmlFormId(1, 'simple');
await Forms.del(form.get());
await assert.throws(() => { purgeForms(true, null, null, 'simple'); }, (err) => {
err.problemCode.should.equal(500.1);
err.problemDetails.error.should.equal('Must also specify projectId when using xmlFormId');
return true;
});
}));

it('should force purge form by project and xmlFormId', testTask(({ Forms }) =>
Forms.getByProjectAndXmlFormId(1, 'simple')
.then((form) => Forms.del(form.get())
.then(() => purgeForms(true, null, 1, 'simple'))
.then((count) => {
count.should.equal(1);
}))));
it('should not purge form by project and xmlFormId if form deleted recently and not forced', testTask(({ Forms }) =>
Forms.getByProjectAndXmlFormId(1, 'simple')
.then((form) => Forms.del(form.get())
.then(() => purgeForms(false, null, 1, 'simple'))
.then((count) => {
count.should.equal(0);
}))));

it('should purge all versions of deleted form in project', testService(async (service, container) => {
const asAlice = await service.login('alice');

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

// new version (will be v2)
await asAlice.post('/v1/projects/1/forms?ignoreWarnings=true')
.send(testData.forms.simple)
.set('Content-Type', 'application/xml')
.expect(200);

// publish new version v2
await asAlice.post('/v1/projects/1/forms/simple/draft/publish?ignoreWarnings=true&version=v2')
.expect(200);

// delete new version v2
await asAlice.delete('/v1/projects/1/forms/simple')
.expect(200);

// new version (will be v3)
await asAlice.post('/v1/projects/1/forms?ignoreWarnings=true')
.send(testData.forms.simple)
.set('Content-Type', 'application/xml')
.expect(200);

// publish new version v3 but don't delete
await asAlice.post('/v1/projects/1/forms/simple/draft/publish?ignoreWarnings=true&version=v3')
.expect(200);

const count = await container.Forms.purge(true, null, 1, 'simple');
count.should.equal(2);
}));

it('should purged named form only from specified project', testService(async (service, container) => {
const asAlice = await service.login('alice');

// delete simple form in project 1 (but don't purge it)
await asAlice.delete('/v1/projects/1/forms/simple')
.expect(200);

const newProjectId = await asAlice.post('/v1/projects')
.send({ name: 'Project Two' })
.then(({ body }) => body.id);

await asAlice.post(`/v1/projects/${newProjectId}/forms?publish=true`)
.send(testData.forms.simple)
.set('Content-Type', 'application/xml')
.expect(200);

await asAlice.delete(`/v1/projects/${newProjectId}/forms/simple`)
.expect(200);

const count = await container.Forms.purge(true, null, newProjectId, 'simple');
count.should.equal(1);
}));
});

});

0 comments on commit 636b3b3

Please sign in to comment.