Skip to content

Commit 376e2bb

Browse files
authored
new: functionality for dataset/:name/entities added (getodk#823)
* new: functionality for dataset/:name/entities added * updated get query to conditionally include submission and form information Added more tests * remove temp migration * use Promise.all instead of waiting in forEach * use Entity.Def.Metadata directly in the extender instead of manipulating the results lower case api in source type * incorporated PR comments
1 parent 71d6bf7 commit 376e2bb

File tree

5 files changed

+241
-75
lines changed

5 files changed

+241
-75
lines changed

lib/model/frames/entity.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99

1010
/* eslint-disable no-multi-spaces */
1111

12-
const { embedded, Frame, table } = require('../frame');
12+
const { embedded, Frame, readable, table } = require('../frame');
1313
const { parseSubmissionXml } = require('../../data/entity');
1414

1515
// These Frames don't interact with APIs directly, hence no readable/writable
1616
class Entity extends Frame.define(
1717
table('entities'),
18-
'id', 'uuid',
18+
'id', 'uuid', readable,
1919
'datasetId',
20-
'createdAt', 'creatorId',
21-
embedded('creator')
20+
'createdAt', readable, 'creatorId', readable,
21+
'updatedAt', readable, 'deletedAt', readable,
22+
embedded('creator'),
23+
embedded('currentVersion')
2224
) {
2325
get def() { return this.aux.def; }
2426

@@ -41,11 +43,24 @@ Entity.Partial = class extends Entity {};
4143

4244
Entity.Def = Frame.define(
4345
table('entity_defs', 'def'),
44-
'id', 'entityId',
45-
'createdAt', 'current',
46-
'submissionDefId',
47-
'label',
48-
'data'
46+
'id', 'entityId',
47+
'createdAt', readable, 'current', readable,
48+
'submissionDefId', 'label', readable,
49+
'creatorId', readable, 'userAgent', readable,
50+
'data', readable,
51+
embedded('creator'),
52+
embedded('source')
4953
);
5054

55+
Entity.Def.Metadata = class extends Entity.Def {
56+
57+
// we don't want `data` to be selected from database, hence return in forApi()
58+
static get fields() {
59+
return super.fields.filter(f => f !== 'data');
60+
}
61+
};
62+
63+
Entity.Def.Source = Frame.define(
64+
'type', readable, 'details', readable
65+
);
5166
module.exports = { Entity };

lib/model/query/entities.js

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
// except according to the terms contained in the LICENSE file.
99

1010
const { sql } = require('slonik');
11-
const { Actor, Entity } = require('../frames');
12-
const { unjoiner, page } = require('../../util/db');
11+
const { Actor, Entity, Submission, Form } = require('../frames');
12+
const { equals, extender, unjoiner, page } = require('../../util/db');
1313
const { map } = require('ramda');
1414
const { construct } = require('../../util/util');
1515
const { QueryOptions } = require('../../util/db');
1616
const { odataFilter } = require('../../data/odata-filter');
1717
const { odataToColumnMap } = require('../../data/entity');
18+
const { isTrue } = require('../../util/http');
1819

1920
////////////////////////////////////////////////////////////////////////////////
2021
// ENTITY CREATE
@@ -119,6 +120,60 @@ const processSubmissionEvent = (event) => (container) =>
119120
////////////////////////////////////////////////////////////////////////////////
120121
// GETTING ENTITIES
121122

123+
const _get = (includeSource) => {
124+
const frames = [Entity];
125+
if (includeSource) {
126+
frames.push(Entity.Def.into('currentVersion'), Submission, Submission.Def.into('submissionDef'), Form);
127+
} else {
128+
frames.push(Entity.Def.Metadata.into('currentVersion'));
129+
}
130+
return extender(...frames)(Actor.into('creator'), Actor.alias('current_version_actors', 'currentVersionCreator'))((fields, extend, options, deleted = false) =>
131+
sql`
132+
SELECT ${fields} FROM entities
133+
INNER JOIN entity_defs
134+
ON entities.id = entity_defs."entityId" AND entity_defs.current
135+
${extend||sql`
136+
LEFT JOIN actors ON actors.id=entities."creatorId"
137+
LEFT JOIN actors current_version_actors ON current_version_actors.id=entity_defs."creatorId"
138+
`}
139+
${!includeSource ? sql`` : sql`
140+
LEFT JOIN submission_defs ON submission_defs.id = entity_defs."submissionDefId"
141+
LEFT JOIN (
142+
SELECT submissions.*, submission_defs."userAgent" FROM submissions
143+
JOIN submission_defs ON submissions.id = submission_defs."submissionId" AND root
144+
) submissions ON submissions.id = submission_defs."submissionId"
145+
LEFT JOIN forms ON submissions."formId" = forms.id
146+
`}
147+
where ${equals(options.condition)} and entities."deletedAt" is ${deleted ? sql`not` : sql``} null
148+
order by entity_defs.id, entities."createdAt" desc, entities.id desc
149+
`);
150+
};
151+
152+
const getById = (datasetId, uuid, options = QueryOptions.none) => ({ maybeOne }) =>
153+
_get(true)(maybeOne, options.withCondition({ datasetId, uuid }), isTrue(options.argData.deleted))
154+
.then(map((entity) => {
155+
const isSourceSubmission = !!entity.aux.currentVersion.submissionDefId;
156+
157+
// TODO: revisit this when working on POST /entities
158+
const source = new Entity.Def.Source({
159+
type: isSourceSubmission ? 'submission' : 'api',
160+
details: isSourceSubmission ? {
161+
xmlFormId: entity.aux.form.xmlFormId,
162+
instanceId: entity.aux.submission.instanceId,
163+
instanceName: entity.aux.submissionDef.instanceName
164+
} : null
165+
});
166+
167+
const currentVersion = new Entity.Def(entity.aux.currentVersion, { creator: entity.aux.currentVersionCreator, source });
168+
169+
return new Entity(entity, { currentVersion, creator: entity.aux.creator });
170+
}));
171+
172+
const getAll = (datasetId, options = QueryOptions.none) => ({ all }) =>
173+
_get(false)(all, options.withCondition({ datasetId }), isTrue(options.argData.deleted))
174+
.then(map((e) => e.withAux('currentVersion', e.aux.currentVersion.withAux('creator', e.aux.currentVersionCreator))));
175+
176+
122177
// This will check for an entity related to any def of the same submission
123178
// as the one specified. Used when trying to reapprove an edited submission.
124179
const getDefBySubmissionId = (submissionId) => ({ maybeOne }) =>
@@ -161,5 +216,6 @@ module.exports = {
161216
createNew, _processSubmissionDef,
162217
processSubmissionEvent, streamForExport,
163218
getDefBySubmissionId, getByUuid,
164-
countByDatasetId
219+
countByDatasetId, getById,
220+
getAll
165221
};

lib/resources/entities.js

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -36,60 +36,24 @@ const getEntity = (i) => ({
3636
creatorId: 1
3737
});
3838

39-
const populateEntities = () => {
40-
const entities = [];
41-
for (let i = 1; i <= 5; i+=1) {
42-
entities.push({
43-
...getEntity(i),
44-
currentVersion: getEntityDef()
45-
});
46-
}
47-
return entities;
48-
};
49-
5039
module.exports = (service, endpoint) => {
5140

52-
service.get('/projects/:id/datasets/:name/entities', endpoint(async ({ Projects }, { params, queryOptions, query }) => {
53-
54-
await Projects.getById(params.id, queryOptions).then(getOrNotFound);
55-
const entities = populateEntities();
41+
service.get('/projects/:projectId/datasets/:name/entities', endpoint(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {
5642

57-
if (queryOptions.extended) {
58-
for (let i = 0; i < 5; i+=1) {
59-
entities[i].currentVersion.creator = getActor();
60-
entities[i].creator = getActor();
61-
}
62-
}
43+
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);
6344

64-
if (query.deleted) {
65-
entities[4].deletedAt = new Date();
66-
return [entities[4]];
67-
}
45+
await auth.canOrReject('entity.list', dataset);
6846

69-
return entities;
47+
return Entities.getAll(dataset.id, queryOptions);
7048
}));
7149

72-
service.get('/projects/:id/datasets/:name/entities/:uuid', endpoint(async ({ Projects }, { params, queryOptions }) => {
50+
service.get('/projects/:projectId/datasets/:name/entities/:uuid', endpoint(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {
7351

74-
await Projects.getById(params.id, queryOptions).then(getOrNotFound);
75-
const entity = getEntity(1);
76-
entity.updatedAt = '2023-01-02T09:00:00.000Z';
77-
entity.currentVersion = {
78-
...getEntityDef(),
79-
source: {
80-
type: 'api',
81-
details: null
82-
},
83-
data: {
84-
firstName: 'Jane',
85-
lastName: 'Roe',
86-
city: 'Toronto'
87-
},
88-
versionNumber: 2,
89-
label: 'Jane Roe',
90-
createdAt: '2023-01-02T09:00:00.000Z'
91-
};
92-
return entity;
52+
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);
53+
54+
await auth.canOrReject('entity.read', dataset);
55+
56+
return Entities.getById(dataset.id, params.uuid, queryOptions).then(getOrNotFound);
9357
}));
9458

9559
service.get('/projects/:id/datasets/:name/entities/:uuid/versions', endpoint(async ({ Projects }, { params, queryOptions }) => {

test/assertions.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ should.Assertion.add('EntityDef', function assertEntityDef() {
358358
this.params = { operator: 'to be an Entity Def (version)' };
359359

360360

361-
this.obj.should.have.property('versionNumber').which.is.a.Number(); // May not be needed
362361
this.obj.should.have.property('label').which.is.a.String();
363362
this.obj.should.have.property('current').which.is.a.Boolean();
364363
this.obj.should.have.property('createdAt').which.is.a.isoDate();
@@ -389,14 +388,14 @@ should.Assertion.add('EntitySource', function Source() {
389388
this.obj.should.have.property('details');
390389

391390
// details are there only in case of type is submission
392-
if (this.obj.details != null) this.obj.details.should.be.SubmissionDetails();
391+
if (this.obj.details != null) this.obj.details.should.be.EntitySourceSubmissionDetails();
393392
});
394393

395394
// Entity Source Submission Details
396-
should.Assertion.add('SubmissionDetails', function SubmissionDetails() {
397-
this.params = { operator: 'have Submission details' };
395+
should.Assertion.add('EntitySourceSubmissionDetails', function SubmissionDetails() {
396+
this.params = { operator: 'have Entity Source Submission details' };
398397

399398
this.obj.should.have.property('xmlFormId').which.is.a.String();
400399
this.obj.should.have.property('instanceId').which.is.a.String();
401-
this.obj.should.have.property('instanceName').which.is.a.String();
400+
this.obj.should.have.property('instanceName'); // can be null
402401
});

0 commit comments

Comments
 (0)