diff --git a/docs/api.yaml b/docs/api.yaml index 30a4073ca..c62767f91 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -43,6 +43,10 @@ info: - OData Data Document for requests of Submissions and Entities now allow use of `$orderby`. - ETag headers on all Blobs. + **Changed**: + + - The [Entity Create](/central-api-entity-management/#creating-an-entity) endpoint will now generate a UUID if the `uuid` parameter is not provided. + ## ODK Central v2023.5 @@ -10068,7 +10072,11 @@ paths: - Entity Management summary: Creating an Entity description: |- - Creates an Entity in the Dataset. Request body takes the JSON representation of the Entity. It should have `uuid` and `label` property in addition to the user-defined properties of the Dataset in `data` property. + Creates an Entity in the Dataset. The request body takes a JSON representation of the Entity, which has the following properties: + + 1. A `data` object containing values for the user-defined Dataset properties. (Not all properties have to have values.) + 2. A `label` property, which cannot be blank or an empty string. (This is used as a human-readable label in Forms that consume Entities.) + 3. An optional `uuid` property. If the `uuid` is not specified, Central will generate a UUID for an Entity with the provided data and label. Value type of all properties is `string`. diff --git a/lib/data/entity.js b/lib/data/entity.js index c2c8b77da..a3b7d589e 100644 --- a/lib/data/entity.js +++ b/lib/data/entity.js @@ -14,6 +14,7 @@ const csv = require('csv-stringify'); const { clone, path, mergeLeft } = require('ramda'); const { Transform } = require('stream'); +const uuid = require('uuid').v4; const { PartialPipe } = require('../util/stream'); const Problem = require('../util/problem'); const { submissionXmlToFieldStream } = require('./submission'); @@ -112,7 +113,7 @@ const extractEntity = (body, propertyNames, existingEntity) => { if (body.uuid && typeof body.uuid !== 'string') throw Problem.user.invalidDataTypeOfParameter({ field: 'uuid', expected: 'string' }); - entity.system.uuid = normalizeUuid(body.uuid); + entity.system.uuid = (body.uuid) ? normalizeUuid(body.uuid) : uuid(); } if (body.label != null) diff --git a/test/integration/api/entities.js b/test/integration/api/entities.js index 12b188e28..7813588f5 100644 --- a/test/integration/api/entities.js +++ b/test/integration/api/entities.js @@ -1302,6 +1302,36 @@ describe('Entities API', () => { }); })); + it('should generate uuid if one is not provided', testDataset(async (service) => { + const asAlice = await service.login('alice'); + + await asAlice.post('/v1/projects/1/datasets/people/entities') + .send({ + label: 'Johnny Doe', + data: { + first_name: 'Johnny', + age: '22' + } + }) + .expect(200) + .then(({ body: person }) => { + person.should.be.an.Entity(); + person.uuid.should.be.a.uuid(); + person.creatorId.should.equal(5); + person.should.have.property('currentVersion').which.is.an.EntityDef(); + person.currentVersion.should.have.property('label').which.equals('Johnny Doe'); + person.currentVersion.should.have.property('data').which.is.eql({ + first_name: 'Johnny', + age: '22' + }); + person.currentVersion.should.have.property('dataReceived').which.is.eql({ + first_name: 'Johnny', + age: '22', + label: 'Johnny Doe' + }); + }); + })); + it('should reject if uuid is not a valid uuid', testDataset(async (service) => { const asAlice = await service.login('alice'); @@ -2095,6 +2125,27 @@ describe('Entities API', () => { logs[0].details.errorMessage.should.equal('Required parameter label missing.'); }); })); + + it('should not create entity if the uuid is missing in the submission', testDataset(async (service, container) => { + const asAlice = await service.login('alice'); + + await asAlice.post('/v1/projects/1/forms/simpleEntity/submissions') + .send(testData.instances.simpleEntity.one + .replace('id="uuid:12345678-1234-4123-8234-123456789abc"', 'id=""')) + .set('Content-Type', 'application/xml') + .expect(200); + + await exhaust(container); + + await asAlice.get('/v1/projects/1/forms/simpleEntity/submissions/one/audits') + .expect(200) + .then(({ body: logs }) => { + logs[0].should.be.an.Audit(); + logs[0].action.should.be.eql('entity.error'); + logs[0].details.problem.problemCode.should.equal(400.2); + logs[0].details.errorMessage.should.equal('Required parameter uuid missing.'); + }); + })); }); describe('entity updates from submissions', () => {