Skip to content

Commit

Permalink
move the endpoint to datasets resource file
Browse files Browse the repository at this point in the history
  • Loading branch information
sadiqkhoja committed Jan 6, 2025
1 parent 74c0ca5 commit a1f8727
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 194 deletions.
19 changes: 18 additions & 1 deletion lib/resources/datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
// except according to the terms contained in the LICENSE file.

const sanitize = require('sanitize-filename');
const { getOrNotFound } = require('../util/promise');
const { getOrNotFound, reject } = require('../util/promise');
const { streamEntityCsv } = require('../data/entity');
const { validateDatasetName, validatePropertyName } = require('../data/dataset');
const { contentDisposition, success, withEtag } = require('../util/http');
const { md5sum } = require('../util/crypto');
const { Dataset } = require('../model/frames');
const Problem = require('../util/problem');
const { QueryOptions } = require('../util/db');
const { entityList } = require('../formats/openrosa');

module.exports = (service, endpoint) => {
service.get('/projects/:id/datasets', endpoint(({ Projects, Datasets }, { auth, params, queryOptions }) =>
Expand Down Expand Up @@ -102,4 +103,20 @@ module.exports = (service, endpoint) => {

return withEtag(serverEtag, csv);
}));

service.get('/projects/:projectId/datasets/:name/integrity', endpoint.openRosa(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);

// Anyone with the verb `entity.list` or anyone with read access on a Form
// that consumes this dataset can call this endpoint.
const canAccessEntityList = await auth.can('entity.list', dataset);
if (!canAccessEntityList) {
await Datasets.canReadForOpenRosa(auth, params.name, params.projectId)
.then(canAccess => canAccess || reject(Problem.user.insufficientRights()));
}

const entities = await Entities.getEntitiesState(dataset.id, queryOptions.allowArgs('id'));

return entityList({ entities });
}));
};
17 changes: 0 additions & 17 deletions lib/resources/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const { Entity } = require('../model/frames');
const Problem = require('../util/problem');
const { diffEntityData, extractBulkSource, getWithConflictDetails } = require('../data/entity');
const { QueryOptions } = require('../util/db');
const { entityList } = require('../formats/openrosa');

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

Expand All @@ -26,22 +25,6 @@ module.exports = (service, endpoint) => {
return Entities.getAll(dataset.id, queryOptions);
}));

service.get('/projects/:projectId/datasets/:name/integrity', endpoint.openRosa(async ({ Datasets, Entities }, { params, auth, queryOptions }) => {
const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);

// Anyone with the verb `entity.list` or anyone with read access on a Form
// that consumes this dataset can call this endpoint.
const canAccessEntityList = await auth.can('entity.list', dataset);
if (!canAccessEntityList) {
await Datasets.canReadForOpenRosa(auth, params.name, params.projectId)
.then(canAccess => canAccess || reject(Problem.user.insufficientRights()));
}

const entities = await Entities.getEntitiesState(dataset.id, queryOptions.allowArgs('id'));

return entityList({ entities });
}));

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

const dataset = await Datasets.get(params.projectId, params.name, true).then(getOrNotFound);
Expand Down
212 changes: 199 additions & 13 deletions test/integration/api/datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,34 @@ const { sql } = require('slonik');
const { QueryOptions } = require('../../../lib/util/db');
const { createConflict } = require('../fixtures/scenarios');
const { omit } = require('ramda');
const xml2js = require('xml2js');

const { exhaust } = require(appRoot + '/lib/worker/worker');
const Option = require(appRoot + '/lib/util/option');

const testEntities = (test) => testService(async (service, container) => {
const asAlice = await service.login('alice');

await asAlice.post(`/v1/projects/1/datasets`)
.send({ name: 'people' });

const uuids = [
'12345678-1234-4123-8234-123456789aaa',
'12345678-1234-4123-8234-123456789abc'
];

uuids.forEach(async _uuid => {
await asAlice.post('/v1/projects/1/datasets/people/entities')
.send({
uuid: _uuid,
label: 'John Doe'
})
.expect(200);
});

await test(service, container);
});

describe('datasets and entities', () => {

describe('creating datasets and properties via the API', () => {
Expand Down Expand Up @@ -239,7 +263,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789aaa,Willow,,5,Alice,0,,1\n'
'12345678-1234-4123-8234-123456789aaa,Willow,,5,Alice,0,,1\n'
);
}));

Expand Down Expand Up @@ -356,7 +380,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,height,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789aaa,redwood,120,,5,Alice,0,,1\n'
'12345678-1234-4123-8234-123456789aaa,redwood,120,,5,Alice,0,,1\n'
);
}));

Expand Down Expand Up @@ -487,7 +511,7 @@ describe('datasets and entities', () => {
logs[0].actorId.should.equal(5);
logs[0].actee.should.be.a.Dataset();
logs[0].actee.name.should.equal('trees');
logs[0].details.properties.should.eql([ 'circumference' ]);
logs[0].details.properties.should.eql(['circumference']);
});
}));

Expand Down Expand Up @@ -926,8 +950,8 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,first_name,age,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789aaa,Jane (30),Jane,30,,5,Alice,0,,1\n' +
'12345678-1234-4123-8234-123456789abc,Alice (88),Alice,88,,5,Alice,0,,1\n'
'12345678-1234-4123-8234-123456789aaa,Jane (30),Jane,30,,5,Alice,0,,1\n' +
'12345678-1234-4123-8234-123456789abc,Alice (88),Alice,88,,5,Alice,0,,1\n'
);

}));
Expand Down Expand Up @@ -958,7 +982,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,first_name,the.age,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789abc,Alice (88),Alice,88,,5,Alice,0,,1\n'
'12345678-1234-4123-8234-123456789abc,Alice (88),Alice,88,,5,Alice,0,,1\n'
);

}));
Expand Down Expand Up @@ -1014,8 +1038,8 @@ describe('datasets and entities', () => {
const withOutTs = text.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, '');
withOutTs.should.be.eql(
'__id,label,f_q1,e_q2,a_q3,c_q4,b_q1,d_q2,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789ccc,one,w,x,y,z,,,,5,Alice,0,,1\n'+
'12345678-1234-4123-8234-123456789bbb,two,,,c,d,a,b,,5,Alice,0,,1\n'+
'12345678-1234-4123-8234-123456789ccc,one,w,x,y,z,,,,5,Alice,0,,1\n' +
'12345678-1234-4123-8234-123456789bbb,two,,,c,d,a,b,,5,Alice,0,,1\n' +
'12345678-1234-4123-8234-123456789aaa,one,,,y,z,w,x,,5,Alice,0,,1\n'
);
}));
Expand Down Expand Up @@ -1088,7 +1112,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,first_name,age,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-111111111aaa,Robert Doe (expired),Robert,,,5,Alice,1,,2\n'
'12345678-1234-4123-8234-111111111aaa,Robert Doe (expired),Robert,,,5,Alice,1,,2\n'
);

}));
Expand Down Expand Up @@ -1133,7 +1157,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,first_name,age,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789abc,Alicia (85),Alicia,85,,5,Alice,1,,2\n'
'12345678-1234-4123-8234-123456789abc,Alicia (85),Alicia,85,,5,Alice,1,,2\n'
);

}));
Expand Down Expand Up @@ -1170,7 +1194,7 @@ describe('datasets and entities', () => {
const withOutTs = result.replace(isoRegex, '');
withOutTs.should.be.eql(
'__id,label,first_name,age,__createdAt,__creatorId,__creatorName,__updates,__updatedAt,__version\n' +
'12345678-1234-4123-8234-123456789abc,Alicia (85),Alicia,85,,5,Alice,2,,3\n'
'12345678-1234-4123-8234-123456789abc,Alicia (85),Alicia,85,,5,Alice,2,,3\n'
);

}));
Expand Down Expand Up @@ -2836,7 +2860,7 @@ describe('datasets and entities', () => {
.expect(200)
.then(({ text }) => {
text.should.equal('name,label,__version,first_name,the.age\n' +
'12345678-1234-4123-8234-123456789abc,Alice (88),1,Alice,88\n');
'12345678-1234-4123-8234-123456789abc,Alice (88),1,Alice,88\n');
});

}));
Expand Down Expand Up @@ -4570,7 +4594,7 @@ describe('datasets and entities', () => {
.expect(200)
.then(({ body }) => {
body.name.should.be.eql('people');
body.properties.map(p => p.name).should.eql([ 'first_name', 'age' ]);
body.properties.map(p => p.name).should.eql(['first_name', 'age']);
});

await asAlice.get('/v1/audits?action=dataset.create')
Expand Down Expand Up @@ -5963,4 +5987,166 @@ describe('datasets and entities', () => {
}));
});
});

// OpenRosa endpoint
describe('GET /datasets/:name/integrity', () => {
it('should return notfound if the dataset does not exist', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.get('/v1/projects/1/datasets/nonexistent/integrity')
.set('X-OpenRosa-Version', '1.0')
.expect(404);
}));

it('should reject if the user cannot read', testEntities(async (service) => {
const asChelsea = await service.login('chelsea');

await asChelsea.get('/v1/projects/1/datasets/people/integrity')
.set('X-OpenRosa-Version', '1.0')
.expect(403);
}));

it('should happily return given no entities', testService(async (service) => {
const asAlice = await service.login('alice');

await asAlice.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.simpleEntity)
.expect(200);

await asAlice.get('/v1/projects/1/datasets/people/integrity')
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
result.data.entities.should.not.have.property('entity');
});
}));

it('should return data for app-user with access to consuming Form', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.withAttachments.replace(/goodone/g, 'people'))
.set('Content-Type', 'application/xml')
.expect(200);

const appUser = await asAlice.post('/v1/projects/1/app-users')
.send({ displayName: 'test' })
.then(({ body }) => body);

await asAlice.post(`/v1/projects/1/forms/withAttachments/assignments/app-user/${appUser.id}`);

await service.get(`/v1/key/${appUser.token}/projects/1/datasets/people/integrity`)
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
result.data.entities.entity.length.should.be.eql(2);
});
}));

it('should reject for app-user if consuming Form is closed', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.post('/v1/projects/1/forms?publish=true')
.send(testData.forms.withAttachments.replace(/goodone/g, 'people'))
.set('Content-Type', 'application/xml')
.expect(200);

const appUser = await asAlice.post('/v1/projects/1/app-users')
.send({ displayName: 'test' })
.then(({ body }) => body);

await asAlice.post(`/v1/projects/1/forms/withAttachments/assignments/app-user/${appUser.id}`);

await asAlice.patch('/v1/projects/1/forms/withAttachments')
.send({ state: 'closed' })
.expect(200);

await service.get(`/v1/key/${appUser.token}/projects/1/datasets/people/integrity`)
.set('X-OpenRosa-Version', '1.0')
.expect(403);
}));

it('should return with correct deleted value', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.expect(200);

await asAlice.get(`/v1/projects/1/datasets/people/integrity`)
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
result.data.entities.entity.length.should.be.eql(2);
const [first, second] = result.data.entities.entity;
first.$.id.should.be.eql('12345678-1234-4123-8234-123456789aaa');
first.deleted.should.be.eql('false');
second.$.id.should.be.eql('12345678-1234-4123-8234-123456789abc');
second.deleted.should.be.eql('true');
});
}));

it('should return purged entities as well', testEntities(async (service, { Entities }) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.expect(200);

await Entities.purge(true);

await asAlice.get(`/v1/projects/1/datasets/people/integrity`)
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
result.data.entities.entity.length.should.be.eql(2);
const [first, second] = result.data.entities.entity;
first.$.id.should.be.eql('12345678-1234-4123-8234-123456789aaa');
first.deleted.should.be.eql('false');
second.$.id.should.be.eql('12345678-1234-4123-8234-123456789abc');
second.deleted.should.be.eql('true');
});
}));

it('should return only queried entities', testEntities(async (service) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.expect(200);

await asAlice.get(`/v1/projects/1/datasets/people/integrity?id=12345678-1234-4123-8234-123456789abc`)
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
const { entity } = result.data.entities;
entity.$.id.should.be.eql('12345678-1234-4123-8234-123456789abc');
entity.deleted.should.be.eql('true');
});
}));

it('should return only queried purged entities', testEntities(async (service, { Entities }) => {
const asAlice = await service.login('alice');

await asAlice.delete('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789abc')
.expect(200);

await asAlice.delete('/v1/projects/1/datasets/people/entities/12345678-1234-4123-8234-123456789aaa')
.expect(200);

await Entities.purge(true);

await asAlice.get(`/v1/projects/1/datasets/people/integrity?id=12345678-1234-4123-8234-123456789abc`)
.set('X-OpenRosa-Version', '1.0')
.expect(200)
.then(async ({ text }) => {
const result = await xml2js.parseStringPromise(text, { explicitArray: false });
const { entity } = result.data.entities;
entity.$.id.should.be.eql('12345678-1234-4123-8234-123456789abc');
entity.deleted.should.be.eql('true');
});
}));
});
});
Loading

0 comments on commit a1f8727

Please sign in to comment.