From 6b8cd1aab125b63f08c604d6c526a23783a34c8d Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Thu, 20 Feb 2025 11:31:57 +0000 Subject: [PATCH 01/10] Actees: use postgres native UUID type for actees.id --- lib/actz.js | 31 +++++++ lib/model/frames.js | 2 +- .../20250212-01-actee-id-as-uuid.js | 81 +++++++++++++++++++ lib/model/query/actees.js | 3 +- lib/model/query/assignments.js | 11 +-- lib/model/query/auth.js | 3 +- lib/model/query/datasets.js | 3 +- lib/model/query/forms.js | 9 ++- lib/model/query/projects.js | 3 +- test/integration/other/knex-migrations.js | 12 +-- 10 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 lib/actz.js create mode 100644 lib/model/migrations/20250212-01-actee-id-as-uuid.js diff --git a/lib/actz.js b/lib/actz.js new file mode 100644 index 000000000..6f2c28eec --- /dev/null +++ b/lib/actz.js @@ -0,0 +1,31 @@ +const { sql } = require('slonik'); + +// Take care when changing these values - they should probably remain identical to those in the db +// migration where they were first defined - "actee-id-as-uuid". +const sqlSpecial = { + '*': sql`'00000000-0000-0000-0000-000000000001'`, // eslint-disable-line key-spacing + actor: sql`'00000000-0000-0000-0000-000000000002'`, // eslint-disable-line key-spacing + group: sql`'00000000-0000-0000-0000-000000000003'`, // eslint-disable-line key-spacing + user: sql`'00000000-0000-0000-0000-000000000004'`, // eslint-disable-line key-spacing + form: sql`'00000000-0000-0000-0000-000000000005'`, // eslint-disable-line key-spacing + submission: sql`'00000000-0000-0000-0000-000000000006'`, // eslint-disable-line key-spacing + field_key: sql`'00000000-0000-0000-0000-000000000007'`, // eslint-disable-line key-spacing + config: sql`'00000000-0000-0000-0000-000000000008'`, // eslint-disable-line key-spacing + project: sql`'00000000-0000-0000-0000-000000000009'`, // eslint-disable-line key-spacing + role: sql`'00000000-0000-0000-0000-000000000010'`, // eslint-disable-line key-spacing + assignment: sql`'00000000-0000-0000-0000-000000000011'`, // eslint-disable-line key-spacing + audit: sql`'00000000-0000-0000-0000-000000000012'`, // eslint-disable-line key-spacing + system: sql`'00000000-0000-0000-0000-000000000013'`, // eslint-disable-line key-spacing + singleUse: sql`'00000000-0000-0000-0000-000000000014'`, // eslint-disable-line key-spacing + dataset: sql`'00000000-0000-0000-0000-000000000015'`, // eslint-disable-line key-spacing + public_link: sql`'00000000-0000-0000-0000-000000000016'`, // eslint-disable-line key-spacing +}; + +const uuidFor = acteeId => { + if (acteeId == null) return null; + else if (Object.prototype.hasOwnProperty.call(sqlSpecial, acteeId)) return sqlSpecial[acteeId]; + else if (acteeId.length === 36) return acteeId; + else throw new Error(`Unexpected acteeId: '${acteeId}'`); +}; + +module.exports = { uuidFor }; diff --git a/lib/model/frames.js b/lib/model/frames.js index bafafa74b..4169a5aea 100644 --- a/lib/model/frames.js +++ b/lib/model/frames.js @@ -45,7 +45,7 @@ class Audit extends Frame.define( 'details', readable, 'loggedAt', readable, 'notes', readable, 'claimed', 'processed', 'lastFailure', 'failures', - fieldTypes(['int4', 'int4', 'text', 'varchar', 'jsonb', 'timestamptz', 'text', 'timestamptz', 'timestamptz', 'timestamptz', 'int4']), + fieldTypes(['int4', 'int4', 'text', 'uuid', 'jsonb', 'timestamptz', 'text', 'timestamptz', 'timestamptz', 'timestamptz', 'int4']), embedded('actor'), embedded('actee') ) { // TODO: sort of duplicative of Audits.log diff --git a/lib/model/migrations/20250212-01-actee-id-as-uuid.js b/lib/model/migrations/20250212-01-actee-id-as-uuid.js new file mode 100644 index 000000000..0668081fe --- /dev/null +++ b/lib/model/migrations/20250212-01-actee-id-as-uuid.js @@ -0,0 +1,81 @@ +// Copyright 2025 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. + +/* eslint-disable key-spacing, indent */ + +const fkTables = [ + 'actors', + 'assignments', + 'audits', + 'datasets', + 'forms', + 'projects', +]; + +// These values must never change. If special actees are added or removed in +// future, it is of no concern to this migration. +const specialActees = { + '*': '00000000-0000-0000-0000-000000000001', + actor: '00000000-0000-0000-0000-000000000002', + group: '00000000-0000-0000-0000-000000000003', + user: '00000000-0000-0000-0000-000000000004', + form: '00000000-0000-0000-0000-000000000005', + submission: '00000000-0000-0000-0000-000000000006', + field_key: '00000000-0000-0000-0000-000000000007', + config: '00000000-0000-0000-0000-000000000008', + project: '00000000-0000-0000-0000-000000000009', + role: '00000000-0000-0000-0000-000000000010', + assignment: '00000000-0000-0000-0000-000000000011', + audit: '00000000-0000-0000-0000-000000000012', + system: '00000000-0000-0000-0000-000000000013', + singleUse: '00000000-0000-0000-0000-000000000014', + dataset: '00000000-0000-0000-0000-000000000015', + public_link: '00000000-0000-0000-0000-000000000016', +}; +if (Object.keys(specialActees).length !== + new Set(Object.values(specialActees)).size) { + throw new Error('Check specialActees values are unique'); +} + +const tableName = t => t.padStart(16, ' '); + +const up = (db) => db.raw(` + -- drop constraints + ${fkTables.map(t => `ALTER TABLE ${tableName(t)} DROP CONSTRAINT IF EXISTS ${t}_acteeid_foreign;`).join('\n ')} + + -- update references to their special values + ${Object.entries(specialActees).flatMap(([ str, uuid ]) => [ + `UPDATE actees SET id='${uuid}' WHERE id='${str}';`, + `UPDATE actees SET species='${uuid}' WHERE species='${str}';`, + ...fkTables.map(t => `UPDATE ${tableName(t)} SET "acteeId"='${uuid}' WHERE "acteeId"='${str}';`), + ]).join('\n ')} + + -- change column types + ALTER TABLE actees ALTER COLUMN id TYPE UUID USING id::UUID; + ALTER TABLE actees ALTER COLUMN parent TYPE UUID USING parent::UUID; + ALTER TABLE actees ALTER COLUMN species TYPE UUID USING species::UUID; + ${fkTables.map(t => `ALTER TABLE ${tableName(t)} ALTER COLUMN "acteeId" TYPE UUID USING "acteeId"::UUID;`).join('\n ')} + + -- add missing special actees + INSERT INTO actees (id, species) VALUES('${specialActees.system}', '${specialActees['*']}'); + INSERT INTO actees (id, species) VALUES('${specialActees.singleUse}', '${specialActees['*']}'); + INSERT INTO actees (id, species) VALUES('${specialActees.dataset}', '${specialActees['*']}'); + INSERT INTO actees (id, species) VALUES('${specialActees.public_link}', '${specialActees['*']}'); + + -- re-add constraints + ALTER TABLE actees ADD CONSTRAINT actees_parent_foreign FOREIGN KEY(parent) REFERENCES actees(id); + ALTER TABLE actees ADD CONSTRAINT actees_species_foreign FOREIGN KEY(species) REFERENCES actees(id); + ${fkTables.map(t => `ALTER TABLE ${tableName(t)} ADD CONSTRAINT ${tableName(t)}_acteeid_foreign FOREIGN KEY("acteeId") REFERENCES actees(id);`).join('\n ')} +`); + +const down = (db) => db.raw(` + -- TODO reverse all statements from up() +`); + +module.exports = { up, down }; diff --git a/lib/model/query/actees.js b/lib/model/query/actees.js index acaedb40b..466445385 100644 --- a/lib/model/query/actees.js +++ b/lib/model/query/actees.js @@ -9,11 +9,12 @@ const uuid = require('uuid').v4; const { sql } = require('slonik'); +const { uuidFor } = require('../../actz'); const { Actee } = require('../frames'); const { construct } = require('../../util/util'); const provision = (species, parent) => ({ one }) => - one(sql`insert into actees (id, species, parent) values (${uuid()}, ${species}, ${(parent == null) ? null : parent.acteeId}) returning *`) + one(sql`insert into actees (id, species, parent) values (${uuid()}, ${uuidFor(species)}, ${uuidFor(parent?.acteeId)}) returning *`) .then(construct(Actee)); module.exports = { provision }; diff --git a/lib/model/query/assignments.js b/lib/model/query/assignments.js index e7c741b28..f9c77fe91 100644 --- a/lib/model/query/assignments.js +++ b/lib/model/query/assignments.js @@ -8,6 +8,7 @@ // except according to the terms contained in the LICENSE file. const { sql } = require('slonik'); +const { uuidFor } = require('../../actz'); const { Actor, Assignment } = require('../frames'); const { extender, sqlEquals, QueryOptions } = require('../../util/db'); const { getOrReject } = require('../../util/promise'); @@ -16,7 +17,7 @@ const { construct } = require('../../util/util'); const _grant = (actor, roleId, acteeId) => ({ one }) => one(sql` insert into assignments ("actorId", "roleId", "acteeId") -values (${actor.id}, ${roleId}, ${acteeId}) +values (${actor.id}, ${roleId}, ${uuidFor(acteeId)}) returning *`) .then(construct(Assignment)); @@ -37,7 +38,7 @@ const grantSystem = (actor, systemName, actee) => ({ Assignments, Roles }) => .then((role) => Assignments.grant(actor, role, actee)); const _revoke = (actor, roleId, acteeId) => ({ db }) => - db.query(sql`delete from assignments where ${sqlEquals({ actorId: actor.id, roleId, acteeId })}`) + db.query(sql`delete from assignments where ${sqlEquals({ actorId: actor.id, roleId, acteeId: uuidFor(acteeId) })}`) .then(({ rowCount }) => Number(rowCount) > 0); _revoke.audit = (actor, roleId, acteeId) => (log) => { @@ -55,7 +56,7 @@ const revoke = (actor, role, actee) => ({ Assignments }) => Assignments._revoke( const revokeByActorId = (actorId) => ({ run }) => run(sql`delete from assignments where "actorId"=${actorId}`); const revokeByActeeId = (acteeId) => ({ run }) => - run(sql`delete from assignments where "acteeId"=${acteeId}`); + run(sql`delete from assignments where "acteeId"=${acteeId}::UUID`); const _get = extender(Assignment)(Actor)((fields, extend, options) => sql` @@ -63,9 +64,9 @@ select ${fields} from assignments ${extend|| sql`inner join actors on actors.id=assignments."actorId"`} where ${sqlEquals(options.condition)}`); const getByActeeId = (acteeId, options = QueryOptions.none) => ({ all }) => - _get(all, options.withCondition({ 'assignments.acteeId': acteeId })); + _get(all, options.withCondition({ 'assignments.acteeId': uuidFor(acteeId) })); const getByActeeAndRoleId = (acteeId, roleId, options = QueryOptions.none) => ({ all }) => - _get(all, options.withCondition({ 'assignments.acteeId': acteeId, roleId })); + _get(all, options.withCondition({ 'assignments.acteeId': uuidFor(acteeId), roleId })); const _getForForms = extender(Assignment, Assignment.FormSummary)(Actor)((fields, extend, options) => sql` select ${fields} from assignments diff --git a/lib/model/query/auth.js b/lib/model/query/auth.js index 6280ee7d9..1223d7419 100644 --- a/lib/model/query/auth.js +++ b/lib/model/query/auth.js @@ -9,6 +9,7 @@ const { sql } = require('slonik'); const { compose, uniq, flatten, map } = require('ramda'); +const { uuidFor } = require('../../actz'); const { Actor, Session } = require('../frames'); const { resolve, reject } = require('../../util/promise'); const Option = require('../../util/option'); @@ -16,7 +17,7 @@ const Problem = require('../../util/problem'); const _impliedActees = (acteeId) => sql` with recursive implied(id) as ( - (select ${acteeId}::varchar) union + (select ${uuidFor(acteeId)}::UUID) union (select unnest(ARRAY[ parent, species ]) from actees join implied on implied.id=actees.id))`; diff --git a/lib/model/query/datasets.js b/lib/model/query/datasets.js index 4e8d870c9..b1a48a7a6 100644 --- a/lib/model/query/datasets.js +++ b/lib/model/query/datasets.js @@ -8,6 +8,7 @@ // except according to the terms contained in the LICENSE file. const { sql } = require('slonik'); +const { uuidFor } = require('../../actz'); const { extender, insert, QueryOptions, sqlEquals, unjoiner, updater } = require('../../util/db'); const { Dataset, Form, Audit } = require('../frames'); const { validatePropertyName } = require('../../data/dataset'); @@ -374,7 +375,7 @@ const _get = extender(Dataset)(Dataset.Extended)((fields, extend, options, publi inner join roles as role on role.id=assignments."roleId" where "actorId"=${actorId}) as assignment - on assignment."acteeId" in ('*', 'project', projects."acteeId") + on assignment."acteeId" in (${uuidFor('*')}, ${uuidFor('project')}, projects."acteeId") group by id having array_agg(distinct verb) @> array['project.read', 'dataset.list'] ) as filtered diff --git a/lib/model/query/forms.js b/lib/model/query/forms.js index 269025ceb..843098de2 100644 --- a/lib/model/query/forms.js +++ b/lib/model/query/forms.js @@ -9,6 +9,7 @@ const { sql } = require('slonik'); const { map } = require('ramda'); +const { uuidFor } = require('../../actz'); const { Frame, into } = require('../frame'); const { Actor, Blob, Form } = require('../frames'); const { getFormFields, merge, compare } = require('../../data/schema'); @@ -559,7 +560,7 @@ inner join inner join (select id from roles where verbs ? 'form.read' or verbs ? 'open_form.read') as role on role.id=assignments."roleId" where "actorId"=${auth.actor.map((actor) => actor.id).orElse(-1)}) as assignment - on assignment."acteeId" in ('*', 'form', projects."acteeId", forms."acteeId") + on assignment."acteeId" in (${uuidFor('*')}, ${uuidFor('form')}, projects."acteeId", forms."acteeId") group by forms.id) as filtered on filtered.id=forms.id where "projectId"=${projectId} and state not in ('closing', 'closed') and "currentDefId" is not null @@ -602,14 +603,14 @@ const _unjoiner = unjoiner(Form, Form.Def); const getByActeeIdForUpdate = (acteeId, options, version) => ({ maybeOne }) => maybeOne(sql` select ${_unjoiner.fields} from forms join form_defs on ${versionJoinCondition(version)} -where "acteeId"=${acteeId} and "deletedAt" is null +where "acteeId"=${acteeId}::UUID and "deletedAt" is null for update`) .then(map(_unjoiner)); const getByActeeId = (acteeId, options, version) => ({ maybeOne }) => maybeOne(sql` select ${_unjoiner.fields} from forms join form_defs on ${versionJoinCondition(version)} -where "acteeId"=${acteeId} and "deletedAt" is null`) +where "acteeId"=${acteeId}::UUID and "deletedAt" is null`) .then(map(_unjoiner)); // there are many combinations of required fields here so we compose our own extender variant. @@ -652,7 +653,7 @@ inner join inner join (select id from roles where verbs ? 'form.update') as role on role.id=assignments."roleId" where "actorId"=${actorId}) as assignment - on assignment."acteeId" in ('*', 'project', projects."acteeId") + on assignment."acteeId" in (${uuidFor('*')}, ${uuidFor('project')}, projects."acteeId") group by id) as filtered on filtered.id=forms."projectId"`} where ${sqlEquals(options.condition)} and forms."deletedAt" is ${deleted ? sql`not` : sql``} null diff --git a/lib/model/query/projects.js b/lib/model/query/projects.js index 097984142..bb8dc1f52 100644 --- a/lib/model/query/projects.js +++ b/lib/model/query/projects.js @@ -8,6 +8,7 @@ // except according to the terms contained in the LICENSE file. const { sql } = require('slonik'); +const { uuidFor } = require('../../actz'); const { Key, Project } = require('../frames'); const { extender, sqlEquals, insert, updater, markDeleted, QueryOptions } = require('../../util/db'); const { generateManagedKey, generateVersionSuffix, stripPemEnvelope } = require('../../util/crypto'); @@ -110,7 +111,7 @@ inner join inner join roles as role on role.id=assignments."roleId" where "actorId"=${actorId}) as assignment - on assignment."acteeId" in ('*', 'project', projects."acteeId") + on assignment."acteeId" in (${uuidFor('*')}, ${uuidFor('project')}, projects."acteeId") group by id having array_agg(distinct verb) @> array['project.read', 'form.list'] or array_agg(distinct verb) @> array['project.read', 'open_form.list'] ) as filtered diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index f1272bb46..fb074bd7d 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -255,7 +255,7 @@ describe('database migrations: removing default project', function() { })); }); -describe('database migrations: intermediate form schema', function() { +describe.skip('database migrations: intermediate form schema', function() { this.timeout(20000); it('should test migration', testServiceFullTrx(async (service, container) => { @@ -384,7 +384,7 @@ describe('database migrations: intermediate form schema', function() { })); }); -describe('database migrations: 20230123-01-remove-google-backups', function() { +describe.skip('database migrations: 20230123-01-remove-google-backups', function() { this.timeout(20000); beforeEach(() => upToMigration('20230123-01-remove-google-backups.js', false)); @@ -608,7 +608,7 @@ describe.skip('database migrations from 20230406: altering entities and entity_d })); }); -describe('database migrations from 20230512: adding entity_def_sources table', function () { +describe.skip('database migrations from 20230512: adding entity_def_sources table', function () { this.timeout(20000); it('should backfill entityId and entityDefId in audit log', testServiceFullTrx(async (service, container) => { @@ -840,7 +840,7 @@ describe('database migrations from 20230512: adding entity_def_sources table', f })); }); -describe('database migrations from 20230802: delete orphan submissions', function test() { +describe.skip('database migrations from 20230802: delete orphan submissions', function test() { this.timeout(20000); it('should delete orphan draft Submissions', testServiceFullTrx(async (service, container) => { @@ -984,7 +984,7 @@ testMigration('20240215-02-dedupe-verbs.js', () => { })); }); -testMigration('20240914-02-remove-orphaned-client-audits.js', () => { +testMigration.skip('20240914-02-remove-orphaned-client-audits.js', () => { it('should remove orphaned client audits', testServiceFullTrx(async (service, container) => { await populateUsers(container); await populateForms(container); @@ -1238,7 +1238,7 @@ testMigration('20240914-02-remove-orphaned-client-audits.js', () => { }); }); -testMigration('20241227-01-backfill-audit-entity-uuid.js', () => { +testMigration.skip('20241227-01-backfill-audit-entity-uuid.js', () => { it('should update the format of detail for entity.delete audits', testServiceFullTrx(async (service, container) => { await populateUsers(container); await populateForms(container); From e3dc1e0d67c5aa80ab267ef97b4de879acf30846 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Thu, 14 Aug 2025 13:48:35 +0000 Subject: [PATCH 02/10] Fix Datasets.canReadForOpenRosa() --- lib/model/query/datasets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/query/datasets.js b/lib/model/query/datasets.js index b1a48a7a6..0a0362a7b 100644 --- a/lib/model/query/datasets.js +++ b/lib/model/query/datasets.js @@ -680,7 +680,7 @@ const canReadForOpenRosa = (auth, datasetName, projectId) => ({ oneFirst }) => o SELECT id FROM roles WHERE verbs ? 'form.read' OR verbs ? 'open_form.read' ) AS role ON role.id=assignments."roleId" WHERE "actorId"=${auth.actor.map((actor) => actor.id).orElse(-1)} - ) AS assignment ON assignment."acteeId" IN ('*', 'form', projects."acteeId", forms."acteeId") + ) AS assignment ON assignment."acteeId" IN (${uuidFor('*')}, ${uuidFor('form')}, projects."acteeId", forms."acteeId") WHERE forms.state != 'closed' GROUP BY forms."xmlFormId" ) AS users_forms ON users_forms."xmlFormId" = linked_forms."xmlFormId" From fe018dfce126920e795ecfd52c2daa1adc7e4354 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Thu, 14 Aug 2025 16:24:29 +0000 Subject: [PATCH 03/10] skip migration tests which rely on prod code --- test/integration/other/knex-migrations.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index 66d960087..97f51d54b 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -922,7 +922,7 @@ describe.skip('database migration: 20231002-01-add-conflict-details.js', functio })); }); -testMigration('20240215-01-entity-delete-verb.js', () => { +testMigration.skip('20240215-01-entity-delete-verb.js', () => { it('should add entity.delete verb to correct roles', testServiceFullTrx(async (service, container) => { await populateUsers(container); @@ -954,7 +954,7 @@ testMigration('20240215-01-entity-delete-verb.js', () => { })); }); -testMigration('20240215-02-dedupe-verbs.js', () => { +testMigration.skip('20240215-02-dedupe-verbs.js', () => { it('should remove duplicate submission.update verb', testServiceFullTrx(async (service, container) => { await populateUsers(container); From d9aec4f5f778c6455938cdf87ecd83d7e1763244 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Sat, 23 Aug 2025 08:01:58 +0000 Subject: [PATCH 04/10] Add missing file header --- lib/actz.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/actz.js b/lib/actz.js index 6f2c28eec..a1f6ba224 100644 --- a/lib/actz.js +++ b/lib/actz.js @@ -1,3 +1,12 @@ +// Copyright 2025 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. + const { sql } = require('slonik'); // Take care when changing these values - they should probably remain identical to those in the db From 88a76797276099c5b4ca32ebd733547084613acd Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Sun, 2 Nov 2025 07:40:31 +0000 Subject: [PATCH 05/10] use UUID v8 --- lib/actz.js | 32 +++++++++---------- .../20250212-01-actee-id-as-uuid.js | 32 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/actz.js b/lib/actz.js index a1f6ba224..3b4ca48f3 100644 --- a/lib/actz.js +++ b/lib/actz.js @@ -12,22 +12,22 @@ const { sql } = require('slonik'); // Take care when changing these values - they should probably remain identical to those in the db // migration where they were first defined - "actee-id-as-uuid". const sqlSpecial = { - '*': sql`'00000000-0000-0000-0000-000000000001'`, // eslint-disable-line key-spacing - actor: sql`'00000000-0000-0000-0000-000000000002'`, // eslint-disable-line key-spacing - group: sql`'00000000-0000-0000-0000-000000000003'`, // eslint-disable-line key-spacing - user: sql`'00000000-0000-0000-0000-000000000004'`, // eslint-disable-line key-spacing - form: sql`'00000000-0000-0000-0000-000000000005'`, // eslint-disable-line key-spacing - submission: sql`'00000000-0000-0000-0000-000000000006'`, // eslint-disable-line key-spacing - field_key: sql`'00000000-0000-0000-0000-000000000007'`, // eslint-disable-line key-spacing - config: sql`'00000000-0000-0000-0000-000000000008'`, // eslint-disable-line key-spacing - project: sql`'00000000-0000-0000-0000-000000000009'`, // eslint-disable-line key-spacing - role: sql`'00000000-0000-0000-0000-000000000010'`, // eslint-disable-line key-spacing - assignment: sql`'00000000-0000-0000-0000-000000000011'`, // eslint-disable-line key-spacing - audit: sql`'00000000-0000-0000-0000-000000000012'`, // eslint-disable-line key-spacing - system: sql`'00000000-0000-0000-0000-000000000013'`, // eslint-disable-line key-spacing - singleUse: sql`'00000000-0000-0000-0000-000000000014'`, // eslint-disable-line key-spacing - dataset: sql`'00000000-0000-0000-0000-000000000015'`, // eslint-disable-line key-spacing - public_link: sql`'00000000-0000-0000-0000-000000000016'`, // eslint-disable-line key-spacing + '*': sql`'00000000-0000-8000-8000-000000000001'`, // eslint-disable-line key-spacing + actor: sql`'00000000-0000-8000-8000-000000000002'`, // eslint-disable-line key-spacing + group: sql`'00000000-0000-8000-8000-000000000003'`, // eslint-disable-line key-spacing + user: sql`'00000000-0000-8000-8000-000000000004'`, // eslint-disable-line key-spacing + form: sql`'00000000-0000-8000-8000-000000000005'`, // eslint-disable-line key-spacing + submission: sql`'00000000-0000-8000-8000-000000000006'`, // eslint-disable-line key-spacing + field_key: sql`'00000000-0000-8000-8000-000000000007'`, // eslint-disable-line key-spacing + config: sql`'00000000-0000-8000-8000-000000000008'`, // eslint-disable-line key-spacing + project: sql`'00000000-0000-8000-8000-000000000009'`, // eslint-disable-line key-spacing + role: sql`'00000000-0000-8000-8000-000000000010'`, // eslint-disable-line key-spacing + assignment: sql`'00000000-0000-8000-8000-000000000011'`, // eslint-disable-line key-spacing + audit: sql`'00000000-0000-8000-8000-000000000012'`, // eslint-disable-line key-spacing + system: sql`'00000000-0000-8000-8000-000000000013'`, // eslint-disable-line key-spacing + singleUse: sql`'00000000-0000-8000-8000-000000000014'`, // eslint-disable-line key-spacing + dataset: sql`'00000000-0000-8000-8000-000000000015'`, // eslint-disable-line key-spacing + public_link: sql`'00000000-0000-8000-8000-000000000016'`, // eslint-disable-line key-spacing }; const uuidFor = acteeId => { diff --git a/lib/model/migrations/20250212-01-actee-id-as-uuid.js b/lib/model/migrations/20250212-01-actee-id-as-uuid.js index 0668081fe..f81985adb 100644 --- a/lib/model/migrations/20250212-01-actee-id-as-uuid.js +++ b/lib/model/migrations/20250212-01-actee-id-as-uuid.js @@ -21,22 +21,22 @@ const fkTables = [ // These values must never change. If special actees are added or removed in // future, it is of no concern to this migration. const specialActees = { - '*': '00000000-0000-0000-0000-000000000001', - actor: '00000000-0000-0000-0000-000000000002', - group: '00000000-0000-0000-0000-000000000003', - user: '00000000-0000-0000-0000-000000000004', - form: '00000000-0000-0000-0000-000000000005', - submission: '00000000-0000-0000-0000-000000000006', - field_key: '00000000-0000-0000-0000-000000000007', - config: '00000000-0000-0000-0000-000000000008', - project: '00000000-0000-0000-0000-000000000009', - role: '00000000-0000-0000-0000-000000000010', - assignment: '00000000-0000-0000-0000-000000000011', - audit: '00000000-0000-0000-0000-000000000012', - system: '00000000-0000-0000-0000-000000000013', - singleUse: '00000000-0000-0000-0000-000000000014', - dataset: '00000000-0000-0000-0000-000000000015', - public_link: '00000000-0000-0000-0000-000000000016', + '*': '00000000-0000-8000-8000-000000000001', + actor: '00000000-0000-8000-8000-000000000002', + group: '00000000-0000-8000-8000-000000000003', + user: '00000000-0000-8000-8000-000000000004', + form: '00000000-0000-8000-8000-000000000005', + submission: '00000000-0000-8000-8000-000000000006', + field_key: '00000000-0000-8000-8000-000000000007', + config: '00000000-0000-8000-8000-000000000008', + project: '00000000-0000-8000-8000-000000000009', + role: '00000000-0000-8000-8000-000000000010', + assignment: '00000000-0000-8000-8000-000000000011', + audit: '00000000-0000-8000-8000-000000000012', + system: '00000000-0000-8000-8000-000000000013', + singleUse: '00000000-0000-8000-8000-000000000014', + dataset: '00000000-0000-8000-8000-000000000015', + public_link: '00000000-0000-8000-8000-000000000016', }; if (Object.keys(specialActees).length !== new Set(Object.values(specialActees)).size) { From 83ebf49d33e2d43d1ebf6400e1c13f0225dda623 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Sun, 2 Nov 2025 08:37:55 +0000 Subject: [PATCH 06/10] add missing foreign keys --- lib/model/migrations/20251103-01-actee-id-as-uuid.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/model/migrations/20251103-01-actee-id-as-uuid.js b/lib/model/migrations/20251103-01-actee-id-as-uuid.js index f81985adb..7554603ef 100644 --- a/lib/model/migrations/20251103-01-actee-id-as-uuid.js +++ b/lib/model/migrations/20251103-01-actee-id-as-uuid.js @@ -72,6 +72,9 @@ const up = (db) => db.raw(` ALTER TABLE actees ADD CONSTRAINT actees_parent_foreign FOREIGN KEY(parent) REFERENCES actees(id); ALTER TABLE actees ADD CONSTRAINT actees_species_foreign FOREIGN KEY(species) REFERENCES actees(id); ${fkTables.map(t => `ALTER TABLE ${tableName(t)} ADD CONSTRAINT ${tableName(t)}_acteeid_foreign FOREIGN KEY("acteeId") REFERENCES actees(id);`).join('\n ')} + + CREATE INDEX idx_fk_actees_species ON "actees" ("species"); + CREATE INDEX idx_fk_datasets_acteeId ON "datasets" ("acteeId"); `); const down = (db) => db.raw(` From 431f7dd6c3f2b76015291add08ec46554cd38fa3 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Wed, 19 Nov 2025 12:23:15 +0000 Subject: [PATCH 07/10] test: add comment why it's now skipped --- test/integration/other/knex-migrations.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index 723eee1cf..8fcc5ddda 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -255,6 +255,9 @@ describe('database migrations: removing default project', function() { })); }); +// skipped: test setup relies on populateUsers(), which relies on application +// code which has changed since the test was written. +// REVIEW: this test will never work again, and should probably be deleted. describe.skip('database migrations: intermediate form schema', function() { this.timeout(20000); From e3655b533e4528b241eceae454f3df63ef1eb60a Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Wed, 19 Nov 2025 12:24:57 +0000 Subject: [PATCH 08/10] skip specific test and add comment explaining --- test/integration/other/knex-migrations.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index 8fcc5ddda..ffbd5b920 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -387,7 +387,7 @@ describe.skip('database migrations: intermediate form schema', function() { })); }); -describe.skip('database migrations: 20230123-01-remove-google-backups', function() { +describe('database migrations: 20230123-01-remove-google-backups', function() { this.timeout(20000); beforeEach(() => upToMigration('20230123-01-remove-google-backups.js', false)); @@ -421,7 +421,10 @@ describe.skip('database migrations: 20230123-01-remove-google-backups', function return actor.id; }; - it('consumes a token', testContainerFullTrx(async (container) => { + // skipped: test setup relies on createToken(), which relies on application + // code which has changed since the test was written. + // REVIEW: this test will never work again, and should probably be deleted. + it.skip('consumes a token', testContainerFullTrx(async (container) => { const actorId = await createToken(container); const { one } = container; const count = () => one(sql`SELECT From 04645f06c6e598766564a9b2dbada9d6f3ace13a Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Wed, 19 Nov 2025 12:25:59 +0000 Subject: [PATCH 09/10] remove no-op skip --- test/integration/other/knex-migrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index ffbd5b920..30cd7c15a 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -614,7 +614,7 @@ describe.skip('database migrations from 20230406: altering entities and entity_d })); }); -describe.skip('database migrations from 20230512: adding entity_def_sources table', function () { +describe('database migrations from 20230512: adding entity_def_sources table', function () { this.timeout(20000); it.skip('should backfill entityId and entityDefId in audit log', testServiceFullTrx(async (service, container) => { From 7358c817ceb34bb8fc680a8154dea54c4f4afaf9 Mon Sep 17 00:00:00 2001 From: alxndrsn Date: Wed, 19 Nov 2025 12:45:56 +0000 Subject: [PATCH 10/10] remove unnecessary commit --- test/integration/other/knex-migrations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/other/knex-migrations.js b/test/integration/other/knex-migrations.js index 30cd7c15a..b79d19c2e 100644 --- a/test/integration/other/knex-migrations.js +++ b/test/integration/other/knex-migrations.js @@ -846,7 +846,7 @@ describe('database migrations from 20230512: adding entity_def_sources table', f })); }); -describe.skip('database migrations from 20230802: delete orphan submissions', function test() { +describe('database migrations from 20230802: delete orphan submissions', function test() { this.timeout(20000); it.skip('should delete orphan draft Submissions', testServiceFullTrx(async (service, container) => {