Skip to content

Commit 71d6bf7

Browse files
authored
Entity migrations (getodk#824)
* Migration for adding creatorId and userAgent to entity_defs * Set creatorId and userAgent on entity def from submission def * Move entity label to entity def and add deletedAt * Updated unit test that didnt like label moving * Change creatorId source and make cols not null * entity_defs userAgent column can be null
1 parent f5efab5 commit 71d6bf7

File tree

8 files changed

+229
-16
lines changed

8 files changed

+229
-16
lines changed

lib/data/entity.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const parseSubmissionXml = (entityFields, xml) => new Promise((resolve, reject)
9494
const formatRow = (entity, props) => {
9595
const out = [];
9696
out.push(entity.uuid);
97-
out.push(entity.label);
97+
out.push(entity.def.label);
9898
for (const prop of props) out.push(entity.def.data[prop]);
9999
return out;
100100
};
@@ -144,7 +144,7 @@ const selectFields = (entity, properties, selectedProperties) => {
144144
}
145145

146146
if (!selectedProperties || selectedProperties.has('label')) {
147-
result.label = entity.label;
147+
result.label = entity.def.label;
148148
}
149149

150150
// Handle System properties

lib/model/frames/entity.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { parseSubmissionXml } = require('../../data/entity');
1616
class Entity extends Frame.define(
1717
table('entities'),
1818
'id', 'uuid',
19-
'datasetId', 'label',
19+
'datasetId',
2020
'createdAt', 'creatorId',
2121
embedded('creator')
2222
) {
@@ -44,6 +44,7 @@ Entity.Def = Frame.define(
4444
'id', 'entityId',
4545
'createdAt', 'current',
4646
'submissionDefId',
47+
'label',
4748
'data'
4849
);
4950

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2023 ODK Central Developers
2+
// See the NOTICE file at the top-level directory of this distribution and at
3+
// https://github.com/getodk/central-backend/blob/master/NOTICE.
4+
// This file is part of ODK Central. It is subject to the license terms in
5+
// the LICENSE file found in the top-level directory of this distribution and at
6+
// https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
7+
// including this file, may be copied, modified, propagated, or distributed
8+
// except according to the terms contained in the LICENSE file.
9+
10+
const up = async (db) => {
11+
// Add columns
12+
await db.raw(`
13+
ALTER TABLE entity_defs
14+
ADD COLUMN "creatorId" integer,
15+
ADD COLUMN "userAgent" varchar(255)
16+
`);
17+
18+
// Assign root entity creator id (same as submitter id) to entity def creator id
19+
await db.raw(`
20+
UPDATE entity_defs
21+
SET "creatorId" = entities."creatorId"
22+
FROM entities
23+
WHERE entity_defs."entityId" = entities.id;
24+
`);
25+
26+
// Assign submission user agent to entity def
27+
await db.raw(`
28+
UPDATE entity_defs
29+
SET "userAgent" = submission_defs."userAgent"
30+
FROM submission_defs
31+
WHERE entity_defs."submissionDefId" = submission_defs.id;
32+
`);
33+
34+
// alter table to make new column not null
35+
await db.raw(`
36+
ALTER TABLE entity_defs
37+
ALTER COLUMN "creatorId" SET NOT NULL
38+
`);
39+
};
40+
41+
const down = (db) => db.raw(`
42+
ALTER TABLE entity_defs
43+
DROP COLUMN "creatorId",
44+
DROP COLUMN "userAgent"
45+
`);
46+
47+
module.exports = { up, down };
48+
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2023 ODK Central Developers
2+
// See the NOTICE file at the top-level directory of this distribution and at
3+
// https://github.com/getodk/central-backend/blob/master/NOTICE.
4+
// This file is part of ODK Central. It is subject to the license terms in
5+
// the LICENSE file found in the top-level directory of this distribution and at
6+
// https://www.apache.org/licenses/LICENSE-2.0. No part of ODK Central,
7+
// including this file, may be copied, modified, propagated, or distributed
8+
// except according to the terms contained in the LICENSE file.
9+
10+
const up = async (db) => {
11+
// Add columns
12+
await db.raw(`
13+
ALTER TABLE entity_defs
14+
ADD COLUMN "label" text
15+
`);
16+
17+
// Assign label from root entity to entity def
18+
// should only be one entity def per entity at the time
19+
// of this migration.
20+
await db.raw(`
21+
UPDATE entity_defs
22+
set "label" = entities."label"
23+
FROM entities
24+
WHERE entity_defs."entityId" = entities.id;
25+
`);
26+
27+
// set label to not null
28+
await db.raw(`
29+
ALTER TABLE entity_defs
30+
ALTER COLUMN "label" SET NOT NULL
31+
`);
32+
33+
await db.raw(`
34+
ALTER TABLE entities
35+
DROP COLUMN "label",
36+
ADD COLUMN "deletedAt" timestamp
37+
`);
38+
};
39+
40+
const down = async (db) => {
41+
await db.raw(`
42+
ALTER TABLE entities
43+
ADD COLUMN "label" text
44+
`);
45+
46+
await db.raw(`
47+
UPDATE entities
48+
set "label" = entity_defs."label"
49+
FROM entity_defs
50+
WHERE entity_defs."entityId" = entities.id;
51+
`);
52+
53+
await db.raw(`
54+
ALTER TABLE entity_defs
55+
DROP COLUMN "label"
56+
`);
57+
58+
await db.raw(`
59+
ALTER TABLE entities
60+
DROP COLUMN "deletedAt",
61+
ALTER COLUMN "label" SET NOT NULL
62+
`);
63+
};
64+
65+
module.exports = { up, down };
66+

lib/model/query/entities.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ FROM submission_defs AS sd
3030
JOIN datasets ON datasets."projectId" = f."projectId"
3131
WHERE datasets."name" = ${dsName} AND sd."id" = ${subDefId}`;
3232

33-
const _defInsert = (id, partial, subDefId, json) => sql`insert into entity_defs ("entityId", "submissionDefId", "data", "current", "createdAt")
34-
values (${id}, ${subDefId}, ${json}, true, clock_timestamp())
33+
const _defInsert = (id, subDefId, creatorId, userAgent, label, json) => sql`insert into entity_defs ("entityId", "submissionDefId", "creatorId", "userAgent", "label", "data", "current", "createdAt")
34+
values (${id}, ${subDefId}, ${creatorId}, ${userAgent}, ${label}, ${json}, true, clock_timestamp())
3535
returning *`;
3636
const nextval = sql`nextval(pg_get_serial_sequence('entities', 'id'))`;
3737

@@ -44,10 +44,10 @@ const nextval = sql`nextval(pg_get_serial_sequence('entities', 'id'))`;
4444
const createNew = (partial, subDef) => ({ one }) => {
4545
const json = JSON.stringify(partial.def.data);
4646
return one(sql`
47-
with def as (${_defInsert(nextval, partial, subDef.id, json)}),
47+
with def as (${_defInsert(nextval, subDef.id, subDef.submitterId, subDef.userAgent, partial.label, json)}),
4848
ds as (${_getDataset(partial.aux.dataset, subDef.id)}),
49-
ins as (insert into entities (id, "datasetId", "uuid", "label", "createdAt", "creatorId")
50-
select def."entityId", ds."id", ${partial.uuid}, ${partial.label}, def."createdAt", ${subDef.submitterId} from def
49+
ins as (insert into entities (id, "datasetId", "uuid", "createdAt", "creatorId")
50+
select def."entityId", ds."id", ${partial.uuid}, def."createdAt", ${subDef.submitterId} from def
5151
join ds on ds."subDefId" = def."submissionDefId"
5252
returning entities.*)
5353
select ins.*, def.id as "entityDefId", ds."acteeId" as "dsActeeId", ds."name" as "dsName" from ins, def, ds;`)
@@ -59,7 +59,8 @@ select ins.*, def.id as "entityDefId", ds."acteeId" as "dsActeeId", ds."name" as
5959
submissionDefId: subDef.id,
6060
createdAt: entityData.createdAt,
6161
creatorId: subDef.submitterId,
62-
data: partial.def.data
62+
data: partial.def.data,
63+
label: partial.label
6364
}),
6465
dataset: { acteeId: dsActeeId, name: dsName }
6566
}));
@@ -94,7 +95,7 @@ const processSubmissionEvent = (event) => (container) =>
9495
.then((entity) => {
9596
if (entity != null) {
9697
return container.Audits.log({ id: event.actorId }, 'entity.create', { acteeId: entity.aux.dataset.acteeId },
97-
{ entity: { uuid: entity.uuid, label: entity.label, dataset: entity.aux.dataset.name },
98+
{ entity: { uuid: entity.uuid, label: entity.def.label, dataset: entity.aux.dataset.name },
9899
submissionId: event.details.submissionId,
99100
submissionDefId: event.details.submissionDefId });
100101
}

test/integration/other/migrations.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,98 @@ describe('database migrations: 20230324-01-edit-dataset-verbs.js', function () {
499499
viewer.verbs.length.should.equal(7);
500500
}));
501501
});
502+
503+
// eslint-disable-next-line func-names
504+
describe('database migrations from 20230406: altering entities and entity_defs', function () {
505+
this.timeout(10000);
506+
507+
const createEntity = async (service, container) => {
508+
// Get bob's id because bob is going to be the one who
509+
// submits the submission
510+
const creatorId = await service.login('bob', (asBob) =>
511+
asBob.get('/v1/users/current').expect(200).then(({ body }) => body.id));
512+
513+
await service.login('bob', (asBob) =>
514+
asBob.post('/v1/projects/1/forms/simple/submissions')
515+
.set('Content-Type', 'application/xml')
516+
.send(testData.instances.simple.one)
517+
.expect(200));
518+
519+
// Get the submission def id we just submitted.
520+
// For this test, it doesn't have to be a real entity submission.
521+
const subDef = await container.one(sql`SELECT id, "userAgent" FROM submission_defs WHERE "instanceId" = 'one' LIMIT 1`);
522+
523+
// Make sure there is a dataset for the entity to be part of.
524+
const newDataset = await container.one(sql`
525+
INSERT INTO datasets
526+
("acteeId", "name", "projectId", "createdAt")
527+
VALUES (${uuid()}, 'trees', 1, now())
528+
RETURNING "id"`);
529+
530+
// Create the entity.
531+
// The root entity creator wont change in this migration though it should be
532+
// the same as the submitter id.
533+
const newEntity = await container.one(sql`
534+
INSERT INTO entities (uuid, "datasetId", "label", "creatorId", "createdAt")
535+
VALUES (${uuid()}, ${newDataset.id}, 'some label', ${creatorId}, now())
536+
RETURNING "id"`);
537+
538+
// Create the entity def and link it to the submission def above.
539+
await container.run(sql`
540+
INSERT INTO entity_defs ("entityId", "createdAt", "current", "submissionDefId", "data")
541+
VALUES (${newEntity.id}, now(), true, ${subDef.id}, '{}')`);
542+
543+
return { subDef, newEntity, creatorId };
544+
};
545+
546+
it('should set entity def creator id and user agent from submission def', testServiceFullTrx(async (service, container) => {
547+
await upToMigration('20230406-01-add-entity-def-fields.js', false);
548+
await populateUsers(container);
549+
await populateForms(container);
550+
551+
const { subDef, newEntity, creatorId } = await createEntity(service, container);
552+
553+
// Apply the migration!!
554+
await up();
555+
556+
// Look up the entity def again
557+
const entityDef = await container.one(sql`SELECT * FROM entity_defs WHERE "entityId" = ${newEntity.id} LIMIT 1`);
558+
559+
// The creatorId and userAgent should now exist and be set.
560+
entityDef.creatorId.should.equal(creatorId);
561+
entityDef.userAgent.should.equal(subDef.userAgent);
562+
563+
await down();
564+
}));
565+
566+
it('should move the entity label to the entity_def table', testServiceFullTrx(async (service, container) => {
567+
await upToMigration('20230406-01-add-entity-def-fields.js', false);
568+
await populateUsers(container);
569+
await populateForms(container);
570+
571+
const { newEntity } = await createEntity(service, container);
572+
573+
// test entity had to be made before applying this migration because of not null creatorId constraint
574+
await up(); // applying 20230406-02-move-entity-label-add-deletedAt.js
575+
576+
// Apply the migration!!
577+
await up();
578+
579+
// Should be able to set deleted at timestamp
580+
await container.run(sql`UPDATE entities SET "deletedAt" = now() WHERE "id" = ${newEntity.id}`);
581+
582+
// Look up the entity def again
583+
const entityDef = await container.one(sql`SELECT * FROM entity_defs WHERE "entityId" = ${newEntity.id} LIMIT 1`);
584+
const entity = await container.one(sql`SELECT * FROM entities WHERE "id" = ${newEntity.id} LIMIT 1`);
585+
586+
// The label should now be on the entity def
587+
(entity.label == null).should.equal(true);
588+
entityDef.label.should.equal('some label');
589+
590+
// Should be able to see the deletedAt timestamp
591+
entity.deletedAt.should.not.be.null();
592+
593+
594+
await down();
595+
}));
596+
});

test/integration/worker/entity.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,12 @@ describe('worker: entity', () => {
220220
const { count } = await container.one(sql`select count(*) from entities`);
221221
count.should.equal(1);
222222

223-
const { label } = await container.one(sql`select label from entities where "uuid" = ${createEvent.details.entity.uuid}`);
223+
const { data, label, creatorId, userAgent } = await container.one(sql`select data, label, "creatorId", "userAgent" from entity_defs`);
224224
label.should.equal('Alice (88)');
225-
226-
const { data } = await container.one(sql`select data from entity_defs`);
227225
data.age.should.equal('88');
228226
data.first_name.should.equal('Alice');
227+
creatorId.should.equal(5); // Alice the user created this entity
228+
userAgent.should.not.be.null();
229229
}));
230230
});
231231

test/unit/data/entity.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ describe('extracting entities from submissions', () => {
196196
describe('selectFields', () => {
197197
const entity = {
198198
uuid: 'uuid',
199-
label: 'label',
200199
createdAt: 'createdAt',
201200
def: {
201+
label: 'label',
202202
data: {
203203
firstName: 'John',
204204
lastName: 'Doe'
@@ -269,9 +269,11 @@ describe('extracting entities from submissions', () => {
269269
it('should return all properties even if entity object does not have all of them', () => {
270270
const data = {
271271
uuid: 'uuid',
272-
label: 'label',
273272
createdAt: 'createdAt',
274-
def: { data: {} },
273+
def: {
274+
label: 'label',
275+
data: {}
276+
},
275277
aux: {
276278
creator: {
277279
id: 'id',

0 commit comments

Comments
 (0)