diff --git a/lib/model/query/forms.js b/lib/model/query/forms.js
index 2e267f26c..5bc524c44 100644
--- a/lib/model/query/forms.js
+++ b/lib/model/query/forms.js
@@ -292,7 +292,20 @@ createVersion.audit = (newDef, partial, form, publish) => (log) => ((publish ===
? log('form.update.publish', form, { oldDefId: form.currentDefId, newDefId: newDef.id })
: log('form.update.draft.set', form, { oldDraftDefId: form.draftDefId, newDraftDefId: newDef.id }));
createVersion.audit.withResult = true;
+createVersion.audit.logEvenIfAnonymous = true;
+
+// This is used in the rare case where we want to change and update a FormDef in place without
+// creating a new def. This is basically a wrapper around _updateDef that also logs an event.
+const replaceDef = (partial, form) => async ({ Forms }) => {
+ const { version, hash, sha, sha256 } = partial.def;
+ await Forms._updateDef(form.def, { xml: partial.xml, version, hash, sha, sha256 });
+ // all this does is changed updatedAt
+ await Forms._update(form, { updatedAt: (new Date()).toISOString() });
+};
+replaceDef.audit = (_, form) => (log) =>
+ log('form.update.draft.replace', form, { upgrade: 'Updated entities-version in form to 2024.1' });
+replaceDef.audit.logEvenIfAnonymous = true;
////////////////////////////////////////////////////////////////////////////////
// PUBLISHING MANAGEMENT
@@ -807,7 +820,7 @@ module.exports = {
_insertFormFields,
_createNew, createNew, _createNewDef, createVersion,
publish, clearDraft,
- _update, update, _updateDef, del, restore, purge,
+ _update, update, _updateDef, replaceDef, del, restore, purge,
clearUnneededDrafts,
setManagedKey,
getByAuthForOpenRosa,
diff --git a/lib/worker/form.js b/lib/worker/form.js
index a44770607..277c408c7 100644
--- a/lib/worker/form.js
+++ b/lib/worker/form.js
@@ -57,8 +57,9 @@ const updateEntitiesVersion = async ({ Forms }, event) => {
const publishedVersion = await Forms.getByProjectAndXmlFormId(projectId, xmlFormId, true, Form.PublishedVersion).then(o => o.orNull());
if (publishedVersion && publishedVersion.currentDefId != null) {
const partial = await _upgradeEntityVersion(publishedVersion);
- if (partial != null)
+ if (partial != null) {
await Forms.createVersion(partial, publishedVersion, true, true);
+ }
}
const draftVersion = await Forms.getByProjectAndXmlFormId(projectId, xmlFormId, true, Form.DraftVersion).then(o => o.orNull());
@@ -66,7 +67,7 @@ const updateEntitiesVersion = async ({ Forms }, event) => {
const partial = await _upgradeEntityVersion(draftVersion);
// update xml and version in place
if (partial != null)
- await Forms._updateDef(draftVersion.def, { xml: partial.xml, version: partial.def.version });
+ await Forms.replaceDef(partial, draftVersion);
}
};
diff --git a/test/integration/other/form-entities-version.js b/test/integration/other/form-entities-version.js
index 2fafa4a06..65b9fd82e 100644
--- a/test/integration/other/form-entities-version.js
+++ b/test/integration/other/form-entities-version.js
@@ -1,4 +1,6 @@
const { readFileSync } = require('fs');
+const should = require('should');
+const config = require('config');
const appRoot = require('app-root-path');
const { testService } = require('../setup');
const testData = require('../../data/xml');
@@ -309,9 +311,234 @@ describe('Update / migrate entities-version within form', () => {
]);
});
}));
+
+ it('should update the formList once the form changes', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+ const domain = config.get('default.env.domain');
+
+ // Publish a form
+ await asAlice.post('/v1/projects/1/forms?publish=true')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ // Create a draft as well
+ await asAlice.post('/v1/projects/1/forms/updateEntity/draft')
+ .expect(200);
+
+ const token = await asAlice.get('/v1/projects/1/forms/updateEntity/draft')
+ .expect(200)
+ .then(({ body }) => body.draftToken);
+
+ await asAlice.get('/v1/projects/1/formList')
+ .set('X-OpenRosa-Version', '1.0')
+ .expect(200)
+ .then(({ text }) => text.should.containEql(`
+ updateEntity
+ updateEntity
+ 1.0
+ md5:e4902c380ef428aa3d35e4ed17ea6c04
+ ${domain}/v1/projects/1/forms/updateEntity.xml
+ `));
+
+ await asAlice.get(`/v1/test/${token}/projects/1/forms/updateEntity/draft/formList`)
+ .set('X-OpenRosa-Version', '1.0')
+ .expect(200)
+ .then(({ text }) => text.should.containEql(`
+ updateEntity
+ updateEntity
+ 1.0
+ md5:e4902c380ef428aa3d35e4ed17ea6c04
+ ${domain}/v1/test/${token}/projects/1/forms/updateEntity/draft.xml
+ `));
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/projects/1/formList')
+ .set('X-OpenRosa-Version', '1.0')
+ .expect(200)
+ .then(({ text }) => text.should.containEql(`
+ updateEntity
+ updateEntity
+ 1.0[upgrade]
+ md5:77292dd9e1ad532bb5a4f7128e0a9596
+ ${domain}/v1/projects/1/forms/updateEntity.xml
+ `));
+
+ await asAlice.get(`/v1/test/${token}/projects/1/forms/updateEntity/draft/formList`)
+ .set('X-OpenRosa-Version', '1.0')
+ .expect(200)
+ .then(({ text }) => text.should.containEql(`
+ updateEntity
+ updateEntity
+ 1.0[upgrade]
+ md5:77292dd9e1ad532bb5a4f7128e0a9596
+ ${domain}/v1/test/${token}/projects/1/forms/updateEntity/draft.xml
+ `));
+ }));
+
+ it('should update the updatedAt timestamps on the form when updating a draft form', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+
+ // Upload a form and publish it
+ await asAlice.post('/v1/projects/1/forms')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ // check updatedAt on the draft form
+ await asAlice.get('/v1/projects/1/forms/updateEntity')
+ .expect(200)
+ .then(({ body }) => {
+ should(body.updatedAt).be.null();
+ });
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/projects/1/forms/updateEntity')
+ .expect(200)
+ .then(({ body }) => {
+ body.updatedAt.should.be.a.recentIsoDate();
+ });
+ }));
+
+ it('should update the updatedAt timestamps on the form when updating a published form', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+
+ // Upload a form and publish it
+ await asAlice.post('/v1/projects/1/forms?publish=true')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ // check updatedAt on the draft form
+ await asAlice.get('/v1/projects/1/forms/updateEntity')
+ .expect(200)
+ .then(({ body }) => {
+ should(body.updatedAt).be.null();
+ });
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/projects/1/forms/updateEntity')
+ .expect(200)
+ .then(({ body }) => {
+ body.updatedAt.should.be.a.recentIsoDate();
+ });
+ }));
});
describe('audit logging and errors', () => {
+ it('should log events about the upgrade for a published form', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+
+ // Upload a form and publish it
+ await asAlice.post('/v1/projects/1/forms?publish=true')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/audits')
+ .expect(200)
+ .then(({ body }) => {
+ const actions = body.map(a => a.action);
+ actions.should.eql([
+ 'form.update.publish',
+ 'upgrade.process.form.entities_version',
+ 'form.update.publish',
+ 'dataset.create',
+ 'form.create',
+ 'user.session.create'
+ ]);
+ });
+ }));
+
+ it('should log events about the upgrade for a draft form', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+
+ // Upload a form and publish it
+ await asAlice.post('/v1/projects/1/forms')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/audits')
+ .expect(200)
+ .then(({ body }) => {
+ const actions = body.map(a => a.action);
+ actions.should.eql([
+ 'form.update.draft.replace',
+ 'upgrade.process.form.entities_version',
+ 'form.create',
+ 'user.session.create'
+ ]);
+ });
+ }));
+
+ it('should log events about the upgrade for a published and draft form', testService(async (service, container) => {
+ const { Forms, Audits } = container;
+ const asAlice = await service.login('alice');
+
+ // Upload a form and publish it
+ await asAlice.post('/v1/projects/1/forms?publish=true')
+ .send(testData.forms.updateEntity)
+ .set('Content-Type', 'application/xml')
+ .expect(200);
+
+ await asAlice.post('/v1/projects/1/forms/updateEntity/draft');
+
+ const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'updateEntity').then(o => o.get());
+ await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId });
+
+ // Run form upgrade
+ await exhaust(container);
+
+ await asAlice.get('/v1/audits')
+ .expect(200)
+ .then(({ body }) => {
+ const actions = body.map(a => a.action);
+ actions.should.eql([
+ 'form.update.draft.replace',
+ 'form.update.publish',
+ 'upgrade.process.form.entities_version',
+ 'form.update.draft.set',
+ 'form.update.publish',
+ 'dataset.create',
+ 'form.create',
+ 'user.session.create'
+ ]);
+ });
+ }));
+
it('should update the audit log event for a successful upgrade', testService(async (service, container) => {
const { Forms, Audits } = container;
const asAlice = await service.login('alice');