diff --git a/lib/data/schema.js b/lib/data/schema.js index 17659c1e3..87b47d130 100644 --- a/lib/data/schema.js +++ b/lib/data/schema.js @@ -641,9 +641,11 @@ const _updateEntityVersion = (xml, oldVersion, newVersion) => new Promise((pass, // If there are any problems with updating the XML, this will just // return the unaltered XML which will then be a clue for the worker // to not change anything about the Form. -const updateEntityForm = (xml, oldVersion, newVersion, suffix) => +// 2022.1 -> 2024.1 forms only have version changed and suffix added. +// 2023.1 -> 2024.1 forms (for updating) also have branchId and trunkVersion added. +const updateEntityForm = (xml, oldVersion, newVersion, suffix, addOfflineParams) => _updateEntityVersion(xml, oldVersion, newVersion) - .then(_addBranchIdAndTrunkVersion) + .then(x => (addOfflineParams ? _addBranchIdAndTrunkVersion(x) : x)) .then(x => addVersionSuffix(x, suffix)) .catch(() => xml); diff --git a/lib/model/migrations/20241029-01-schedule-entity-form-upgrade-create-forms.js b/lib/model/migrations/20241029-01-schedule-entity-form-upgrade-create-forms.js new file mode 100644 index 000000000..793898943 --- /dev/null +++ b/lib/model/migrations/20241029-01-schedule-entity-form-upgrade-create-forms.js @@ -0,0 +1,34 @@ +// Copyright 2024 ODK Central Developers +// See the NOTICE file at the top-level directory of this distribution and at +// https://github.com/getodk/central-backend/blob/master/NOTICE. +// This file is part of ODK Central. It is subject to the license terms in +// the LICENSE file found in the top-level directory of this distribution and at +// https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central, +// including this file, may be copied, modified, propagated, or distributed +// except according to the terms contained in the LICENSE file. + +// The previous migration only added this event for forms with an +// update action, which should have been entity spec version 2023.1.0. +// We also need to flag 2022.1.0 forms with the create action. +// To avoid flagging forms that do both create + update twice, this +// migration captures the complement set of forms. +// Basically, every existing form should be flagged, but I didn't want +// to change an old migration. + +const up = (db) => db.raw(` + INSERT INTO audits ("action", "acteeId", "loggedAt", "details") + SELECT 'upgrade.process.form.entities_version', forms."acteeId", clock_timestamp(), + '{"upgrade": "As part of upgrading Central to v2024.3, this form is being updated to the latest entities-version spec."}' + FROM forms + JOIN form_defs fd ON forms."id" = fd."formId" + JOIN dataset_form_defs dfd ON fd."id" = dfd."formDefId" + JOIN projects ON projects."id" = forms."projectId" + WHERE NOT dfd."actions" @> '["update"]' + AND forms."deletedAt" IS NULL + AND projects."deletedAt" IS NULL + GROUP BY forms."acteeId"; +`); + +const down = () => {}; + +module.exports = { up, down }; diff --git a/lib/worker/form.js b/lib/worker/form.js index 02bdd3af2..7b9225b19 100644 --- a/lib/worker/form.js +++ b/lib/worker/form.js @@ -44,10 +44,21 @@ const updateDraftSet = pushDraftToEnketo; const updatePublish = pushFormToEnketo; const _upgradeEntityVersion = async (form) => { - const xml = await updateEntityForm(form.xml, '2023.1.0', '2024.1.0', '[upgrade]'); - // If the XML doesnt change (not the version in question, or a parsing error), don't return the new partial Form + // We need to upgrade both 2022.1 and 2023.1 forms to 2024, and we are not sure which it is + // without parsing the form. + // Try one upgrade and then the other. + + // Attempt the 2023.1 upgrade first: + let xml = await updateEntityForm(form.xml, '2023.1.0', '2024.1.0', '[upgrade]', true); + + // If the XML doesnt change (not the version in question, or a parsing error), try the 2022.1 upgrade: + if (xml === form.xml) + xml = await updateEntityForm(form.xml, '2022.1.0', '2024.1.0', '[upgrade]', false); + + // If the XML still has not changed, don't return a partial. if (xml === form.xml) return null; + const partial = await Form.fromXml(xml); return partial.withAux('xls', { xlsBlobId: form.def.xlsBlobId }); }; diff --git a/test/integration/other/form-entities-version.js b/test/integration/other/form-entities-version.js index eeae5a613..9d7cf92ba 100644 --- a/test/integration/other/form-entities-version.js +++ b/test/integration/other/form-entities-version.js @@ -29,6 +29,29 @@ const upgradedUpdateEntity = ` `; +const upgradedSimpleEntity = ` + + + + + + + + + + + + + + + + + + + +`; + describe('Update / migrate entities-version within form', () => { describe('upgrading a 2023.1.0 update form', () => { it('should upgrade a form with only a published version', testService(async (service, container) => { @@ -469,6 +492,49 @@ describe('Update / migrate entities-version within form', () => { })); }); + describe('upgrading a 2022.1.0 create form', () => { + it('should upgrade a form with a draft version and a published version', 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.simpleEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + // Convert the published form to a draft + await asAlice.post('/v1/projects/1/forms/simpleEntity/draft'); + + const { acteeId } = await Forms.getByProjectAndXmlFormId(1, 'simpleEntity').then(o => o.get()); + await Audits.log(null, 'upgrade.process.form.entities_version', { acteeId }); + + // Run form upgrade + await exhaust(container); + + // The version on the draft does change even though it is updated in place + await asAlice.get('/v1/projects/1/forms/simpleEntity/draft') + .then(({ body }) => { + body.version.should.equal('1.0[upgrade]'); + }); + + await asAlice.get('/v1/projects/1/forms/simpleEntity/versions') + .then(({ body }) => { + body.length.should.equal(2); + body[0].version.should.equal('1.0[upgrade]'); + body[1].version.should.equal('1.0'); + }); + + // The published form XML is updated + await asAlice.get('/v1/projects/1/forms/simpleEntity.xml') + .then(({ text }) => text.should.equal(upgradedSimpleEntity)); + + // The draft XML is updated + await asAlice.get('/v1/projects/1/forms/simpleEntity/draft.xml') + .then(({ text }) => text.should.equal(upgradedSimpleEntity)); + })); + }); + describe('audit logging and errors', () => { it('should log events about the upgrade for a published form', testService(async (service, container) => { const { Forms, Audits } = container; diff --git a/test/unit/data/schema.js b/test/unit/data/schema.js index 1ac10a285..7d6b3afd5 100644 --- a/test/unit/data/schema.js +++ b/test/unit/data/schema.js @@ -2089,7 +2089,7 @@ describe('form schema', () => { describe('updateEntityForm', () => { it('should change version 2023->2024, add trunkVersion, and add branchId', (async () => { - const result = await updateEntityForm(testData.forms.updateEntity, '2023.1.0', '2024.1.0', '[upgrade]'); + const result = await updateEntityForm(testData.forms.updateEntity, '2023.1.0', '2024.1.0', '[upgrade]', true); // entities-version has been updated // version has suffix // trunkVersion and branchId are present @@ -2116,14 +2116,53 @@ describe('form schema', () => { `); })); + it('should change version 2022->2024', (async () => { + const result = await updateEntityForm(testData.forms.simpleEntity, '2022.1.0', '2024.1.0', '[upgrade]', false); + // entities-version has been updated + // version has suffix + // trunkVersion and branchId are NOT added + result.should.equal(` + + + + + + + + + + + + + + + + + + + +`); + })); + + // updateEntityForm takes the old version to replace as an argument + // these tests show it will not change a 2022.1 (create-only) form when 2023.1 is provided it('should not alter a version 2022.1.0 form when the old version to replace is 2023.1.0', (async () => { - const result = await updateEntityForm(testData.forms.simpleEntity, '2023.1.0', '2024.1.0', '[upgrade]'); + const result = await updateEntityForm(testData.forms.simpleEntity, '2023.1.0', '2024.1.0', '[upgrade]', true); result.should.equal(testData.forms.simpleEntity); })); + it('should not alter a version 2024.1.0 form when the old version to replace is 2023.1.0', (async () => { - const result = await updateEntityForm(testData.forms.offlineEntity, '2023.1.0', '2024.1.0', '[upgrade]'); + const result = await updateEntityForm(testData.forms.offlineEntity, '2023.1.0', '2024.1.0', '[upgrade]', true); result.should.equal(testData.forms.offlineEntity); })); + + // these tests show it will not change a 2023.1 (update) form when 2022.1 is provided + it('should not alter a version 2023.1.0 form when the old version to replace is 2022.1.0', (async () => { + const result = await updateEntityForm(testData.forms.updateEntity, '2022.1.0', '2024.1.0', '[upgrade]', true); + result.should.equal(testData.forms.updateEntity); + })); + }); });