From 31e0f2e34cfb62efc75a0497649ff4c76b181104 Mon Sep 17 00:00:00 2001 From: Kathleen Tuite Date: Fri, 19 Apr 2024 13:07:30 -0700 Subject: [PATCH] Handle dataset name capitalization conflicts in API --- lib/model/query/datasets.js | 20 +++++++- test/integration/api/datasets.js | 87 +++++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/lib/model/query/datasets.js b/lib/model/query/datasets.js index 69fb4c79a..52b8c5686 100644 --- a/lib/model/query/datasets.js +++ b/lib/model/query/datasets.js @@ -131,7 +131,25 @@ const createOrMerge = (parsedDataset, form, fields) => async ({ one, Actees, Dat }; // Insert a Dataset when there is no associated form and immediately publish -const createPublishedDataset = (dataset, project) => async ({ one, Actees }) => { +const createPublishedDataset = (dataset, project) => async ({ one, Actees, Datasets }) => { + + // Check for existing dataset with the same name + const existingDataset = await Datasets.get(project.id, dataset.name, false); + if (existingDataset.isDefined()) { + const { publishedAt, id: datasetId } = existingDataset.get(); + if (publishedAt) + throw Problem.user.uniquenessViolation({ table: 'datasets', fields: [ 'name', 'projectId' ], values: [ dataset.name, project.id ], }); + + // If existing dataset is only a draft, allow it to be published + return new Dataset(await one(sql`UPDATE datasets SET "publishedAt" = "createdAt" where id = ${datasetId} RETURNING *`)); + } + + // Check for a published dataset with a similar name but different capitalization + const conflictingName = await Datasets.getPublishedBySimilarName(project.id, dataset.name); + if (conflictingName.isDefined()) + throw Problem.user.datasetNameConflict({ current: conflictingName.get().name, provided: dataset.name }); + + // Final case: publish a completely new dataset const actee = await Actees.provision('dataset', project); const dsWithId = await one(insert(dataset.with({ acteeId: actee.id }))); return new Dataset(await one(sql`UPDATE datasets SET "publishedAt" = "createdAt" where id = ${dsWithId.id} RETURNING *`)); diff --git a/test/integration/api/datasets.js b/test/integration/api/datasets.js index 7d7d40fa1..801e4ba78 100644 --- a/test/integration/api/datasets.js +++ b/test/integration/api/datasets.js @@ -98,22 +98,81 @@ describe('datasets and entities', () => { }); })); - it('should reject if creating a dataset that already exists', testService(async (service) => { - const asAlice = await service.login('alice'); + describe('dataset name conflicts', () => { + it('should reject if creating a dataset that already exists', testService(async (service) => { + const asAlice = await service.login('alice'); - await asAlice.post('/v1/projects/1/datasets') - .send({ - name: 'trees' - }) - .expect(200); + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'trees' + }) + .expect(200); - // Second time - await asAlice.post('/v1/projects/1/datasets') - .send({ - name: 'trees' - }) - .expect(409); - })); + // Second time + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'trees' + }) + .expect(409) + .then(({ body }) => { + body.code.should.equal(409.3); + body.message.should.startWith('A resource already exists with name,projectId value(s) of trees,'); + }); + })); + + it('should reject if creating a dataset that has a similar name to an existing published dataset', testService(async (service) => { + const asAlice = await service.login('alice'); + + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'trees' + }) + .expect(200); + + // Second time + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'TREES' + }) + .expect(409) + .then(({ body }) => { + body.code.should.equal(409.16); + body.message.should.startWith("A dataset named 'trees' exists and you provided 'TREES'"); + }); + })); + + it('should allow creating a dataset that only exists as a draft', testService(async (service) => { + const asAlice = await service.login('alice'); + + // draft "people" dataset + await asAlice.post('/v1/projects/1/forms') + .send(testData.forms.simpleEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'people' + }) + .expect(200); + })); + + it('should allow creating a dataset that has a similar name to a draft dataset', testService(async (service) => { + const asAlice = await service.login('alice'); + + // draft "people" dataset + await asAlice.post('/v1/projects/1/forms') + .send(testData.forms.simpleEntity) + .set('Content-Type', 'application/xml') + .expect(200); + + await asAlice.post('/v1/projects/1/datasets') + .send({ + name: 'PEOPLE' + }) + .expect(200); + })); + }); it('should add label-only entity to dataset, all via API', testService(async (service) => { const asAlice = await service.login('alice');