mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
refactor(API): Migrate DELETE /tags/{id} to the decorator pattern (no-changelog) (#38469)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Uddish Verma <uddish.verma@n8n.io>
This commit is contained in:
parent
e36572e582
commit
2e588363e0
|
|
@ -163,7 +163,6 @@ export default defineConfig(
|
|||
'./src/public-api/v1/handlers/security-policy/security-policy.handler.ts',
|
||||
'./src/public-api/v1/handlers/sso-oidc/sso-oidc.handler.ts',
|
||||
'./src/public-api/v1/handlers/sso-saml/sso-saml.handler.ts',
|
||||
'./src/public-api/v1/handlers/tags/tags.handler.ts',
|
||||
'./src/public-api/v1/handlers/users/users.handler.ee.ts',
|
||||
'./src/public-api/v1/handlers/workflows/workflows.handler.ts',
|
||||
],
|
||||
|
|
@ -276,7 +275,6 @@ export default defineConfig(
|
|||
'./src/eventbus/message-event-bus/message-event-bus.ts',
|
||||
'./src/evaluation.ee/evaluation-collection.service.ts',
|
||||
'./src/evaluation.ee/test-runner/test-runner.service.ee.ts',
|
||||
'./src/public-api/v1/handlers/tags/tags.handler.ts',
|
||||
// modules/** non-persistence services surfaced by narrowing the exemption
|
||||
'./src/modules/agents/agent-knowledge.service.ts',
|
||||
'./src/modules/agents/agent-publish.service.ts',
|
||||
|
|
|
|||
|
|
@ -63,10 +63,6 @@ export declare namespace TestRunRequest {
|
|||
type Cancel = AuthenticatedRequest<{ id: string; runId: string }>;
|
||||
}
|
||||
|
||||
export declare namespace TagRequest {
|
||||
type Delete = AuthenticatedRequest<{ id: string }>;
|
||||
}
|
||||
|
||||
export declare namespace CredentialTypeRequest {
|
||||
type Get = AuthenticatedRequest<{ credentialTypeName: string }, {}, {}, {}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
ApiSummary,
|
||||
ApiTags,
|
||||
Body,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
|
|
@ -151,4 +152,29 @@ export class TagsPublicController {
|
|||
...(updatedTag.updatedAt ? { updatedAt: updatedTag.updatedAt.toISOString() } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('/:tagId')
|
||||
@ApiKeyScope('tag:delete')
|
||||
@ApiSummary('Delete a tag')
|
||||
@ApiDescription('Deletes a tag.')
|
||||
@ApiTags(tags)
|
||||
@ApiResponse(200, TagPublicDto)
|
||||
@ApiErrorResponse(404)
|
||||
async deleteTag(
|
||||
_req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('tagId', tagIdParamSchema) tagId: string,
|
||||
): Promise<TagPublicDto> {
|
||||
let tag: TagEntity;
|
||||
|
||||
try {
|
||||
tag = await this.tagService.getById(tagId);
|
||||
} catch {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
await this.tagService.delete(tagId);
|
||||
|
||||
return toTagPublicDto(tag);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
operationId: deleteTag
|
||||
tags:
|
||||
- Tags
|
||||
summary: Delete a tag
|
||||
description: Deletes a tag.
|
||||
x-required-scope: tag:delete
|
||||
x-eov-operation-id: unreachable
|
||||
x-eov-operation-handler: v1/handlers/decorator-routed.handler
|
||||
x-decorator-routed: true
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
required: true
|
||||
description: The ID of the tag.
|
||||
name: tagId
|
||||
in: path
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../../../../shared/spec/schemas/tagPublicDto.generated.yml
|
||||
'401':
|
||||
$ref: ../../../../shared/spec/responses/unauthorized.yml
|
||||
'403':
|
||||
$ref: ../../../../shared/spec/responses/forbidden.yml
|
||||
'404':
|
||||
$ref: ../../../../shared/spec/responses/notFound.yml
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
delete:
|
||||
x-eov-operation-id: deleteTag
|
||||
x-required-scope: tag:delete
|
||||
x-eov-operation-handler: v1/handlers/tags/tags.handler
|
||||
tags:
|
||||
- Tags
|
||||
summary: Delete a tag
|
||||
description: Deletes a tag.
|
||||
parameters:
|
||||
- $ref: '../schemas/parameters/tagId.yml'
|
||||
responses:
|
||||
'200':
|
||||
description: Operation successful.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../schemas/tag.yml'
|
||||
'401':
|
||||
$ref: '../../../../shared/spec/responses/unauthorized.yml'
|
||||
'403':
|
||||
$ref: '../../../../shared/spec/responses/forbidden.yml'
|
||||
'404':
|
||||
$ref: '../../../../shared/spec/responses/notFound.yml'
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
name: id
|
||||
in: path
|
||||
description: The ID of the tag.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { Container } from '@n8n/di';
|
||||
|
||||
import type { TagRequest } from '../../../types';
|
||||
import type { PublicAPIEndpoint } from '../../shared/handler.types';
|
||||
import { apiKeyHasScopeWithGlobalScopeFallback } from '../../shared/middlewares/global.middleware';
|
||||
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { TagService } from '@/services/tag.service';
|
||||
|
||||
type TagHandlers = {
|
||||
deleteTag: PublicAPIEndpoint<TagRequest.Delete>;
|
||||
};
|
||||
|
||||
const tagHandlers: TagHandlers = {
|
||||
deleteTag: [
|
||||
apiKeyHasScopeWithGlobalScopeFallback({ scope: 'tag:delete' }),
|
||||
async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
let tag;
|
||||
try {
|
||||
tag = await Container.get(TagService).getById(id);
|
||||
} catch (error) {
|
||||
throw new NotFoundError('Not Found');
|
||||
}
|
||||
|
||||
await Container.get(TagService).delete(id);
|
||||
return res.json(tag);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export = tagHandlers;
|
||||
|
|
@ -188,6 +188,8 @@ paths:
|
|||
$ref: ./handlers/tags/spec/paths/getTag.generated.yml
|
||||
put:
|
||||
$ref: ./handlers/tags/spec/paths/updateTag.generated.yml
|
||||
delete:
|
||||
$ref: ./handlers/tags/spec/paths/deleteTag.generated.yml
|
||||
/variables:
|
||||
get:
|
||||
$ref: ./handlers/variables/spec/paths/getVariables.generated.yml
|
||||
|
|
|
|||
|
|
@ -97,8 +97,6 @@ paths:
|
|||
$ref: './handlers/credentials/spec/paths/credentials.id.test.yml'
|
||||
/credentials/schema/{credentialTypeName}:
|
||||
$ref: './handlers/credentials/spec/paths/credentials.schema.id.yml'
|
||||
/tags/{id}:
|
||||
$ref: './handlers/tags/spec/paths/tags.id.yml'
|
||||
/workflows/{id}/{versionId}:
|
||||
$ref: './handlers/workflows/spec/paths/workflows.id.versionId.yml'
|
||||
/workflows/{id}/test-runs:
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ TestRunWorkflowId:
|
|||
$ref: '../../../handlers/evaluations/spec/schemas/parameters/testRunWorkflowId.yml'
|
||||
TestRunId:
|
||||
$ref: '../../../handlers/evaluations/spec/schemas/parameters/testRunId.yml'
|
||||
TagId:
|
||||
$ref: '../../../handlers/tags/spec/schemas/parameters/tagId.yml'
|
||||
UserIdentifier:
|
||||
$ref: '../../../handlers/users/spec/schemas/parameters/userIdentifier.yml'
|
||||
IncludeRole:
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ describe('DELETE /tags/:id', () => {
|
|||
const response = await authOwnerAgent.delete('/tags/gZqmqiGAuo1dHT7q');
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.body.message).toBe('Not Found');
|
||||
});
|
||||
|
||||
test('owner should delete the tag', async () => {
|
||||
|
|
@ -171,12 +172,12 @@ describe('DELETE /tags/:id', () => {
|
|||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { id, name, createdAt, updatedAt } = response.body;
|
||||
|
||||
expect(id).toEqual(tag.id);
|
||||
expect(name).toEqual(tag.name);
|
||||
expect(createdAt).toEqual(tag.createdAt.toISOString());
|
||||
expect(updatedAt).toEqual(tag.updatedAt.toISOString());
|
||||
expect(response.body).toStrictEqual({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
createdAt: tag.createdAt.toISOString(),
|
||||
updatedAt: tag.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
// make sure the tag actually deleted from the db
|
||||
const deletedTag = await Container.get(TagRepository).findOneBy({
|
||||
|
|
@ -205,6 +206,23 @@ describe('DELETE /tags/:id', () => {
|
|||
|
||||
expect(notDeletedTag).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should fail due to missing "tag:delete" scope', async () => {
|
||||
const tag = await createTag({});
|
||||
|
||||
const memberWithoutScope = await createMemberWithApiKey({ scopes: ['tag:list'] });
|
||||
const agent = testServer.publicApiAgentFor(memberWithoutScope);
|
||||
|
||||
const response = await agent.delete(`/tags/${tag.id}`);
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
expect(response.body.message).toBe('Forbidden');
|
||||
|
||||
// make sure the tag was not deleted from the db
|
||||
const notDeletedTag = await Container.get(TagRepository).findOneBy({ id: tag.id });
|
||||
|
||||
expect(notDeletedTag).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /tags', () => {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
"GET /tags/{tagId}": {
|
||||
"status": "gap"
|
||||
},
|
||||
"DELETE /tags/{id}": {
|
||||
"DELETE /tags/{tagId}": {
|
||||
"status": "gap"
|
||||
},
|
||||
"PUT /tags/{tagId}": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user