Skip to content

Commit b9aa839

Browse files
committed
feat: added DELETE /api/tags/:tagId endpoint
1 parent 0aad7ea commit b9aa839

8 files changed

Lines changed: 296 additions & 10 deletions

File tree

lib/database/repositories/TagRepository.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ class TagRepository {
5959
const result = await Tag.create(TagAdapter.toDatabase(entity));
6060
return TagAdapter.toEntity(result);
6161
}
62+
63+
/**
64+
* Removes a specific entity.
65+
*
66+
* @param {Object} queryBuilder The QueryBuilder to use.
67+
* @returns {Promise|Null} Promise object representing the full mock data
68+
*/
69+
async removeOne(queryBuilder) {
70+
queryBuilder.limit(1);
71+
72+
const tag = await this.findOne(queryBuilder);
73+
if (tag) {
74+
await Tag.destroy(queryBuilder.toImplementation());
75+
}
76+
77+
return tag;
78+
}
6279
}
6380

6481
module.exports = new TagRepository();

lib/server/controllers/tags.controller.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
* or submit itself to any jurisdiction.
1212
*/
1313

14-
const { tag: { CreateTagUseCase, GetAllTagsUseCase, GetLogsByTagUseCase, GetTagUseCase } } = require('../../usecases');
14+
const {
15+
tag: {
16+
CreateTagUseCase,
17+
DeleteTagUseCase,
18+
GetAllTagsUseCase,
19+
GetLogsByTagUseCase,
20+
GetTagUseCase,
21+
},
22+
} = require('../../usecases');
1523
const { dtos: { CreateTagDto, GetAllTagsDto, GetTagDto } } = require('../../domain');
1624
const { dtoValidator } = require('../utilities');
1725

@@ -64,15 +72,29 @@ const createTag = async (request, response, next) => {
6472
* next middleware function.
6573
* @returns {undefined}
6674
*/
67-
const deleteTag = (request, response, next) => {
68-
response.status(501).json({
69-
errors: [
70-
{
71-
status: '501',
72-
title: 'Not implemented',
73-
},
74-
],
75-
});
75+
const deleteTag = async (request, response, next) => {
76+
const value = await dtoValidator(GetTagDto, request, response);
77+
if (!value) {
78+
return;
79+
}
80+
81+
const logs = await new DeleteTagUseCase()
82+
.execute(value);
83+
84+
if (logs === null) {
85+
response.status(404).json({
86+
errors: [
87+
{
88+
status: '404',
89+
title: `Tag with this id (${value.params.tagId}) could not be found`,
90+
},
91+
],
92+
});
93+
} else {
94+
response.status(200).json({
95+
data: logs,
96+
});
97+
}
7698
};
7799

78100
/**
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
const {
15+
repositories: {
16+
TagRepository,
17+
},
18+
utilities: {
19+
QueryBuilder,
20+
TransactionHelper,
21+
},
22+
} = require('../../database');
23+
24+
/**
25+
* DeleteTagUseCase
26+
*/
27+
class DeleteTagUseCase {
28+
/**
29+
* Executes this use case.
30+
*
31+
* @param {Object} dto The GetAllLogs DTO which contains all request data.
32+
* @returns {Promise} Promise object represents the result of this use case.
33+
*/
34+
async execute(dto = {}) {
35+
const queryBuilder = new QueryBuilder();
36+
const { params } = dto;
37+
const { tagId } = params;
38+
39+
queryBuilder.where('id', tagId);
40+
41+
return TransactionHelper.provide(() => TagRepository.removeOne(queryBuilder));
42+
}
43+
}
44+
45+
module.exports = DeleteTagUseCase;

lib/usecases/tag/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
*/
1313

1414
const CreateTagUseCase = require('./CreateTagUseCase');
15+
const DeleteTagUseCase = require('./DeleteTagUseCase');
1516
const GetAllTagsUseCase = require('./GetAllTagsUseCase');
1617
const GetLogsByTagUseCase = require('./GetLogsByTagUseCase');
1718
const GetTagUseCase = require('./GetTagUseCase');
1819

1920
module.exports = {
2021
CreateTagUseCase,
22+
DeleteTagUseCase,
2123
GetAllTagsUseCase,
2224
GetLogsByTagUseCase,
2325
GetTagUseCase,

spec/openapi.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ paths:
154154
$ref: '#/components/responses/UnexpectedError'
155155
tags:
156156
- tag
157+
delete:
158+
operationId: deleteTagById
159+
summary: Deletes a tag by Id
160+
responses:
161+
'200':
162+
$ref: '#/components/responses/Tag'
163+
'400':
164+
$ref: '#/components/responses/BadRequest'
165+
'404':
166+
$ref: '#/components/responses/NotFound'
167+
default:
168+
$ref: '#/components/responses/UnexpectedError'
169+
tags:
170+
- tag
157171
/tags/{tagId}/logs:
158172
parameters:
159173
- $ref: '#/components/parameters/TagId'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
const { repositories: { TagRepository } } = require('../../../../lib/database');
15+
const { tag: { CreateTagUseCase, DeleteTagUseCase } } = require('../../../../lib/usecases');
16+
const { dtos: { CreateTagDto } } = require('../../../../lib/domain');
17+
const chai = require('chai');
18+
19+
const { expect } = chai;
20+
21+
module.exports = () => {
22+
let createdTag;
23+
24+
beforeEach(async () => {
25+
const createTagDto = await CreateTagDto.validateAsync({
26+
body: {
27+
text: `TAG#${new Date().getTime()}`,
28+
},
29+
});
30+
31+
createdTag = await new CreateTagUseCase()
32+
.execute(createTagDto);
33+
});
34+
35+
it('should remove a tag', async () => {
36+
const nTagsBefore = await TagRepository.count();
37+
38+
await new DeleteTagUseCase()
39+
.execute({
40+
params: {
41+
tagId: createdTag.id,
42+
},
43+
});
44+
45+
const nTagsAfter = await TagRepository.count();
46+
expect(nTagsAfter).to.be.lessThan(nTagsBefore);
47+
});
48+
49+
it('should return the removed tag', async () => {
50+
const nTagsBefore = await TagRepository.count();
51+
52+
const result = await new DeleteTagUseCase()
53+
.execute({
54+
params: {
55+
tagId: createdTag.id,
56+
},
57+
});
58+
59+
const nTagsAfter = await TagRepository.count();
60+
expect(nTagsAfter).to.be.lessThan(nTagsBefore);
61+
62+
expect(result.id).to.equal(createdTag.id);
63+
expect(result.text).to.equal(createdTag.text);
64+
});
65+
};

test/application/usecases/tag/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
*/
1313

1414
const CreateTagUseCase = require('./CreateTagUseCase.test');
15+
const DeleteTagUseCase = require('./DeleteTagUseCase.test');
1516
const GetAllTagsUseCase = require('./GetAllTagsUseCase.test');
1617
const GetTagUseCase = require('./GetTagUseCase.test');
1718

1819
module.exports = () => {
1920
describe('CreateTagUseCase', CreateTagUseCase);
21+
describe('DeleteTagUseCase', DeleteTagUseCase);
2022
describe('GetAllTagsUseCase', GetAllTagsUseCase);
2123
describe('GetTagUseCase', GetTagUseCase);
2224
};

test/e2e/tags.test.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const path = require('path');
1515
const chai = require('chai');
1616
const request = require('supertest');
1717
const chaiResponseValidator = require('chai-openapi-response-validator');
18+
const { tag: { CreateTagUseCase } } = require('../../lib/usecases');
19+
const { dtos: { CreateTagDto } } = require('../../lib/domain');
1820

1921
const { expect } = chai;
2022

@@ -300,6 +302,123 @@ module.exports = () => {
300302
});
301303
});
302304

305+
describe('DELETE /api/tags/:tagId', () => {
306+
let createdTag;
307+
308+
beforeEach(async () => {
309+
const createTagDto = await CreateTagDto.validateAsync({
310+
body: {
311+
text: `TAG#${new Date().getTime()}`,
312+
},
313+
});
314+
315+
createdTag = await new CreateTagUseCase()
316+
.execute(createTagDto);
317+
});
318+
319+
it('should return 400 if the tag id is not a number', (done) => {
320+
request(server)
321+
.delete('/api/tags/abc')
322+
.expect(400)
323+
.end((err, res) => {
324+
if (err) {
325+
done(err);
326+
return;
327+
}
328+
329+
// Response must satisfy the OpenAPI specification
330+
expect(res).to.satisfyApiSpec;
331+
332+
const { errors } = res.body;
333+
const titleError = errors.find((err) => err.source.pointer === '/data/attributes/params/tagId');
334+
expect(titleError.detail).to.equal('"params.tagId" must be a number');
335+
336+
done();
337+
});
338+
});
339+
340+
it('should return 400 if the tag id is not positive', (done) => {
341+
request(server)
342+
.delete('/api/tags/-1')
343+
.expect(400)
344+
.end((err, res) => {
345+
if (err) {
346+
done(err);
347+
return;
348+
}
349+
350+
// Response must satisfy the OpenAPI specification
351+
expect(res).to.satisfyApiSpec;
352+
353+
const { errors } = res.body;
354+
const titleError = errors.find((err) => err.source.pointer === '/data/attributes/params/tagId');
355+
expect(titleError.detail).to.equal('"params.tagId" must be a positive number');
356+
357+
done();
358+
});
359+
});
360+
361+
it('should return 400 if the tag id is not a whole number', (done) => {
362+
request(server)
363+
.delete('/api/tags/0.5')
364+
.expect(400)
365+
.end((err, res) => {
366+
if (err) {
367+
done(err);
368+
return;
369+
}
370+
371+
// Response must satisfy the OpenAPI specification
372+
expect(res).to.satisfyApiSpec;
373+
374+
const { errors } = res.body;
375+
const titleError = errors.find((err) => err.source.pointer === '/data/attributes/params/tagId');
376+
expect(titleError.detail).to.equal('"params.tagId" must be an integer');
377+
378+
done();
379+
});
380+
});
381+
382+
it('should return 404 if the tag could not be found', (done) => {
383+
request(server)
384+
.delete('/api/tags/999999999')
385+
.expect(404)
386+
.end((err, res) => {
387+
if (err) {
388+
done(err);
389+
return;
390+
}
391+
392+
// Response must satisfy the OpenAPI specification
393+
expect(res).to.satisfyApiSpec;
394+
395+
expect(res.body.errors[0].title).to.equal('Tag with this id (999999999) could not be found');
396+
397+
done();
398+
});
399+
});
400+
401+
it('should return 200 in all other cases', (done) => {
402+
request(server)
403+
.delete(`/api/tags/${createdTag.id}`)
404+
.expect(200)
405+
.end((err, res) => {
406+
if (err) {
407+
done(err);
408+
return;
409+
}
410+
411+
// Response must satisfy the OpenAPI specification
412+
expect(res).to.satisfyApiSpec;
413+
414+
expect(res.body.data.id).to.equal(createdTag.id);
415+
expect(res.body.data.text).to.equal(createdTag.text);
416+
417+
done();
418+
});
419+
});
420+
});
421+
303422
describe('GET /api/tags/:tagId/logs', () => {
304423
it('should return 400 if the tag id is not a number', (done) => {
305424
request(server)

0 commit comments

Comments
 (0)