diff --git a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js index 95c7ffbc..dd62c96d 100644 --- a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js +++ b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js @@ -11,5 +11,5 @@ export default async (request, response) => { await accessToken.revoke(); - response.status(204).send(); + response.status(204).end(); }; diff --git a/packages/backend/src/controllers/api/v1/steps/delete-step.js b/packages/backend/src/controllers/api/v1/steps/delete-step.js new file mode 100644 index 00000000..74cfb7b4 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/delete-step.js @@ -0,0 +1,9 @@ +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .findById(request.params.stepId) + .throwIfNotFound(); + + await step.delete(); + + response.status(204).end(); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/delete-step.test.js b/packages/backend/src/controllers/api/v1/steps/delete-step.test.js new file mode 100644 index 00000000..756eb4af --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/delete-step.test.js @@ -0,0 +1,134 @@ +import { describe, it, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createConnection } from '../../../../../test/factories/connection'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; + +describe('DELETE /api/v1/steps/:stepId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should remove the step of the current user and return no content', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + const currentUserConnection = await createConnection(); + + await createStep({ + flowId: currentUserFlow.id, + connectionId: currentUserConnection.id, + }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + connectionId: currentUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .delete(`/api/v1/steps/${actionStep.id}`) + .set('Authorization', token) + .expect(204); + }); + + it('should remove the step of the another user and return no content', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + const anotherUserConnection = await createConnection(); + + await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + }); + + const actionStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .delete(`/api/v1/steps/${actionStep.id}`) + .set('Authorization', token) + .expect(204); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingStepUUID = Crypto.randomUUID(); + + await request(app) + .delete(`/api/v1/steps/${notExistingStepUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .delete('/api/v1/steps/invalidStepUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 5a3e8a25..41561224 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -63,6 +63,10 @@ const authorizationList = { action: 'read', subject: 'Execution', }, + 'DELETE /api/v1/steps/:stepId': { + action: 'update', + subject: 'Flow', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index d2d9ce05..6e6e08ee 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -207,7 +207,9 @@ class Step extends Base { const additionalFields = setupField.additionalFields; if (additionalFields) { - const keyArgument = additionalFields.arguments.find(argument => argument.name === 'key'); + const keyArgument = additionalFields.arguments.find( + (argument) => argument.name === 'key' + ); const dynamicFieldsKey = keyArgument.value; const dynamicFields = await this.createDynamicFields( @@ -289,6 +291,25 @@ class Step extends Base { return this; } + + async delete() { + await this.$relatedQuery('executionSteps').delete(); + await this.$query().delete(); + + const flow = await this.$relatedQuery('flow'); + + const nextSteps = await flow + .$relatedQuery('steps') + .where('position', '>', this.position); + + const nextStepQueries = nextSteps.map(async (nextStep) => { + await nextStep.$query().patch({ + position: nextStep.position - 1, + }); + }); + + await Promise.all(nextStepQueries); + } } export default Step; diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js index e6594483..91db5111 100644 --- a/packages/backend/src/routes/api/v1/steps.js +++ b/packages/backend/src/routes/api/v1/steps.js @@ -6,6 +6,7 @@ import getConnectionAction from '../../../controllers/api/v1/steps/get-connectio import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js'; import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js'; +import deleteStepAction from '../../../controllers/api/v1/steps/delete-step.js'; const router = Router(); @@ -37,4 +38,11 @@ router.post( asyncHandler(createDynamicDataAction) ); +router.delete( + '/:stepId', + authenticateUser, + authorizeUser, + asyncHandler(deleteStepAction) +); + export default router;