Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions lib/model/frames/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@

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

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

// These Frames don't interact with APIs directly, hence no readable/writable
class Entity extends Frame.define(
table('entities'),
'id', 'uuid',
'id', 'uuid', readable,
'datasetId',
'createdAt', 'creatorId',
embedded('creator')
'createdAt', readable, 'creatorId', readable,
'updatedAt', readable, 'deletedAt', readable,
embedded('creator'),
embedded('currentVersion')
) {
get def() { return this.aux.def; }

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

Entity.Def = Frame.define(
table('entity_defs', 'def'),
'id', 'entityId',
'createdAt', 'current',
'submissionDefId',
'label',
'data'
'id', 'entityId',
'createdAt', readable, 'current', readable,
'submissionDefId', 'label', readable,
'creatorId', readable, 'userAgent', readable,
'data', readable,
embedded('creator'),
embedded('source')
);

Entity.Def.Metadata = class extends Entity.Def {

// we don't want `data` to be selected from database, hence return in forApi()
static get fields() {
return super.fields.filter(f => f !== 'data');
}
};

Entity.Def.Source = Frame.define(
'type', readable, 'details', readable
);
module.exports = { Entity };
103 changes: 96 additions & 7 deletions lib/model/query/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
// except according to the terms contained in the LICENSE file.

const { sql } = require('slonik');
const { Actor, Entity } = require('../frames');
const { unjoiner, page } = require('../../util/db');
const { Actor, Entity, Submission, Form } = require('../frames');
const { equals, extender, unjoiner, page } = require('../../util/db');
const { map } = require('ramda');
const { construct } = require('../../util/util');
const { QueryOptions } = require('../../util/db');
const { odataFilter } = require('../../data/odata-filter');
const { odataToColumnMap } = require('../../data/entity');
const { isTrue } = require('../../util/http');

////////////////////////////////////////////////////////////////////////////////
// ENTITY CREATE
Expand Down Expand Up @@ -119,6 +120,96 @@ const processSubmissionEvent = (event) => (container) =>
////////////////////////////////////////////////////////////////////////////////
// GETTING ENTITIES

const _get = (includeSource) => {
const frames = [Entity];
if (includeSource) {
frames.push(Entity.Def.into('currentVersion'), Submission, Submission.Def.into('submissionDef'), Form);
} else {
frames.push(Entity.Def.Metadata.into('currentVersion'));
}
return extender(...frames)(Actor.into('creator'), Actor.alias('current_version_actors', 'currentVersionCreator'))((fields, extend, options, deleted = false) =>
sql`
SELECT ${fields} FROM entities
INNER JOIN entity_defs
ON entities.id = entity_defs."entityId" AND entity_defs.current
${extend||sql`
LEFT JOIN actors ON actors.id=entities."creatorId"
LEFT JOIN actors current_version_actors ON current_version_actors.id=entity_defs."creatorId"
`}
${!includeSource ? sql`` : sql`
LEFT JOIN submission_defs ON submission_defs.id = entity_defs."submissionDefId"
LEFT JOIN (
SELECT submissions.*, submission_defs."userAgent" FROM submissions
JOIN submission_defs ON submissions.id = submission_defs."submissionId" AND root
) submissions ON submissions.id = submission_defs."submissionId"
LEFT JOIN forms ON submissions."formId" = forms.id
`}
where ${equals(options.condition)} and entities."deletedAt" is ${deleted ? sql`not` : sql``} null
order by entity_defs.id, entities."createdAt" desc, entities.id desc
`);
};

const getById = (datasetId, uuid, options = QueryOptions.none) => ({ maybeOne }) =>
_get(true)(maybeOne, options.withCondition({ datasetId, uuid }), isTrue(options.argData.deleted))
.then(map((entity) => {
const isSourceSubmission = !!entity.aux.currentVersion.submissionDefId;

// TODO: revisit this when working on POST /entities
const source = new Entity.Def.Source({
type: isSourceSubmission ? 'submission' : 'api',
details: isSourceSubmission ? {
xmlFormId: entity.aux.form.xmlFormId,
instanceId: entity.aux.submission.instanceId,
instanceName: entity.aux.submissionDef.instanceName
} : null
});

const currentVersion = new Entity.Def(entity.aux.currentVersion, { creator: entity.aux.currentVersionCreator, source });

return new Entity(entity, { currentVersion, creator: entity.aux.creator });
}));

const getAll = (datasetId, options = QueryOptions.none) => ({ all }) =>
_get(false)(all, options.withCondition({ datasetId }), isTrue(options.argData.deleted))
.then(map((e) => e.withAux('currentVersion', e.aux.currentVersion.withAux('creator', e.aux.currentVersionCreator))));

////////////////////////////////////////////////////////////////////////////////
// GETTING ENTITY DEFS

const _getDef = extender(Entity.Def, Submission, Submission.Def.into('submissionDef'), Form)(Actor.into('creator'))((fields, extend, options) => sql`
SELECT ${fields} FROM entities
JOIN entity_defs ON entities.id = entity_defs."entityId"
LEFT JOIN submission_defs ON submission_defs.id = entity_defs."submissionDefId"
LEFT JOIN (
SELECT submissions.*, submission_defs."userAgent" FROM submissions
JOIN submission_defs ON submissions.id = submission_defs."submissionId" AND root
) submissions ON submissions.id = submission_defs."submissionId"
LEFT JOIN forms ON submissions."formId" = forms.id
${extend||sql`
LEFT JOIN actors ON actors.id=entity_defs."creatorId"
`}
where ${equals(options.condition)} AND entities."deletedAt" IS NULL
order by entity_defs."createdAt", entity_defs.id
`);

const getAllDefs = (datasetId, uuid, options = QueryOptions.none) => ({ all }) =>
_getDef(all, options.withCondition({ datasetId, uuid }))
.then(map((v) => {
const isSourceSubmission = !!v.submissionDefId;

// TODO: revisit this when working on POST /entities
const source = new Entity.Def.Source({
type: isSourceSubmission ? 'submission' : 'api',
details: isSourceSubmission ? {
xmlFormId: v.aux.form.xmlFormId,
instanceId: v.aux.submission.instanceId,
instanceName: v.aux.submissionDef.instanceName
} : null
});

return new Entity.Def(v, { creator: v.aux.creator, source });
}));

// This will check for an entity related to any def of the same submission
// as the one specified. Used when trying to reapprove an edited submission.
const getDefBySubmissionId = (submissionId) => ({ maybeOne }) =>
Expand All @@ -128,9 +219,6 @@ const getDefBySubmissionId = (submissionId) => ({ maybeOne }) =>
where s.id = ${submissionId} limit 1`)
.then(map(construct(Entity.Def)));

const getByUuid = (uuid) => ({ maybeOne }) =>
maybeOne(sql`select * from entities where "uuid" = ${uuid} limit 1`)
.then(map(construct(Entity)));


////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -160,6 +248,7 @@ SELECT count(*) FROM entities
module.exports = {
createNew, _processSubmissionDef,
processSubmissionEvent, streamForExport,
getDefBySubmissionId, getByUuid,
countByDatasetId
getDefBySubmissionId,
countByDatasetId, getById,
getAll, getAllDefs
};
87 changes: 21 additions & 66 deletions lib/resources/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
// including this file, may be copied, modified, propagated, or distributed
// except according to the terms contained in the LICENSE file.

const { getOrNotFound } = require('../util/promise');
const { getOrNotFound, reject } = require('../util/promise');
const { mergeLeft } = require('ramda');
const { success } = require('../util/http');
const Problem = require('../util/problem');

const getActor = () => ({
id: 1,
Expand All @@ -36,84 +37,38 @@ const getEntity = (i) => ({
creatorId: 1
});

const populateEntities = () => {
const entities = [];
for (let i = 1; i <= 5; i+=1) {
entities.push({
...getEntity(i),
currentVersion: getEntityDef()
});
}
return entities;
};

module.exports = (service, endpoint) => {

service.get('/projects/:id/datasets/:name/entities', endpoint(async ({ Projects }, { params, queryOptions, query }) => {
service.get('/projects/:projectId/datasets/:name/entities', endpoint(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {

await Projects.getById(params.id, queryOptions).then(getOrNotFound);
const entities = populateEntities();

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

if (query.deleted) {
entities[4].deletedAt = new Date();
return [entities[4]];
}
await auth.canOrReject('entity.list', dataset);

return entities;
return Entities.getAll(dataset.id, queryOptions);
}));

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

await Projects.getById(params.id, queryOptions).then(getOrNotFound);
const entity = getEntity(1);
entity.updatedAt = '2023-01-02T09:00:00.000Z';
entity.currentVersion = {
...getEntityDef(),
source: {
type: 'api',
details: null
},
data: {
firstName: 'Jane',
lastName: 'Roe',
city: 'Toronto'
},
versionNumber: 2,
label: 'Jane Roe',
createdAt: '2023-01-02T09:00:00.000Z'
};
return entity;
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);

await auth.canOrReject('entity.read', dataset);

return Entities.getById(dataset.id, params.uuid, queryOptions).then(getOrNotFound);
}));

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

await Projects.getById(params.id, queryOptions).then(getOrNotFound);
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);

const versions = ['John Doe', 'Jane Doe', 'Richard Roe', 'Janie Doe', 'Baby Roe'].map((name, i) => ({
...getEntityDef(),
current: i === 4,
source: {
type: 'api',
details: null
},
data: {
firstName: name.split(' ')[0],
lastName: name.split(' ')[1],
city: 'Toronto'
},
versionNumber: i+1,
label: name,
createdAt: new Date()
}));
await auth.canOrReject('entity.read', dataset);

const defs = await Entities.getAllDefs(dataset.id, params.uuid, queryOptions);

// it means there's no entity with the provided UUID
if (defs.length === 0) return reject(Problem.user.notFound());

return versions;
return defs;
}));

// May not be needed for now
Expand Down
9 changes: 4 additions & 5 deletions test/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,6 @@ should.Assertion.add('EntityDef', function assertEntityDef() {
this.params = { operator: 'to be an Entity Def (version)' };


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

// details are there only in case of type is submission
if (this.obj.details != null) this.obj.details.should.be.SubmissionDetails();
if (this.obj.details != null) this.obj.details.should.be.EntitySourceSubmissionDetails();
});

// Entity Source Submission Details
should.Assertion.add('SubmissionDetails', function SubmissionDetails() {
this.params = { operator: 'have Submission details' };
should.Assertion.add('EntitySourceSubmissionDetails', function SubmissionDetails() {
this.params = { operator: 'have Entity Source Submission details' };

this.obj.should.have.property('xmlFormId').which.is.a.String();
this.obj.should.have.property('instanceId').which.is.a.String();
this.obj.should.have.property('instanceName').which.is.a.String();
this.obj.should.have.property('instanceName'); // can be null
});
Loading