diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js new file mode 100644 index 00000000..f90d243b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js @@ -0,0 +1,23 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const execution = await request.currentUser.authorizedExecutions + .clone() + .withSoftDeleted() + .findById(request.params.executionId) + .throwIfNotFound(); + + const executionStepsQuery = execution + .$relatedQuery('executionSteps') + .withSoftDeleted() + .withGraphFetched('step') + .orderBy('created_at', 'asc'); + + const executionSteps = await paginateRest( + executionStepsQuery, + request.query.page + ); + + renderObject(response, executionSteps); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js new file mode 100644 index 00000000..fc43ec8b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js @@ -0,0 +1,153 @@ +import { describe, it, expect, 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 { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createExecution } from '../../../../../test/factories/execution.js'; +import { createExecutionStep } from '../../../../../test/factories/execution-step.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getExecutionStepsMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution-steps'; + +describe('GET /api/v1/executions/:executionId/execution-steps', () => { + let currentUser, currentUserRole, anotherUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + anotherUser = await createUser(); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the execution steps of current user execution', async () => { + const currentUserFlow = await createFlow({ + userId: currentUser.id, + }); + + const stepOne = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + }); + + const currentUserExecution = await createExecution({ + flowId: currentUserFlow.id, + }); + + const currentUserExecutionStepOne = await createExecutionStep({ + executionId: currentUserExecution.id, + stepId: stepOne.id, + }); + + const currentUserExecutionStepTwo = await createExecutionStep({ + executionId: currentUserExecution.id, + stepId: stepTwo.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/executions/${currentUserExecution.id}/execution-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionStepsMock( + [currentUserExecutionStepOne, currentUserExecutionStepTwo], + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the execution steps of another user execution', async () => { + const anotherUserFlow = await createFlow({ + userId: anotherUser.id, + }); + + const stepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const anotherUserExecution = await createExecution({ + flowId: anotherUserFlow.id, + }); + + const anotherUserExecutionStepOne = await createExecutionStep({ + executionId: anotherUserExecution.id, + stepId: stepOne.id, + }); + + const anotherUserExecutionStepTwo = await createExecutionStep({ + executionId: anotherUserExecution.id, + stepId: stepTwo.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/executions/${anotherUserExecution.id}/execution-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionStepsMock( + [anotherUserExecutionStepOne, anotherUserExecutionStepTwo], + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing execution step UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingExcecutionUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/executions/${notExistingExcecutionUUID}/execution-steps`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/executions/invalidExecutionUUID/execution-steps') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 07331f30..28b719a2 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -19,6 +19,10 @@ const authorizationList = { action: 'read', subject: 'Execution', }, + 'GET /api/v1/executions/:executionId/execution-steps': { + action: 'read', + subject: 'Execution', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/routes/api/v1/executions.js b/packages/backend/src/routes/api/v1/executions.js index f62daa5c..769b29b6 100644 --- a/packages/backend/src/routes/api/v1/executions.js +++ b/packages/backend/src/routes/api/v1/executions.js @@ -4,6 +4,7 @@ import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import getExecutionsAction from '../../../controllers/api/v1/executions/get-executions.js'; import getExecutionAction from '../../../controllers/api/v1/executions/get-execution.js'; +import getExecutionStepsAction from '../../../controllers/api/v1/executions/get-execution-steps.js'; const router = Router(); @@ -21,4 +22,11 @@ router.get( asyncHandler(getExecutionAction) ); +router.get( + '/:executionId/execution-steps', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionStepsAction) +); + export default router; diff --git a/packages/backend/src/serializers/execution-step.js b/packages/backend/src/serializers/execution-step.js new file mode 100644 index 00000000..52d7a409 --- /dev/null +++ b/packages/backend/src/serializers/execution-step.js @@ -0,0 +1,21 @@ +import stepSerializer from './step.js'; + +const executionStepSerializer = (executionStep) => { + let executionStepData = { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + }; + + if (executionStep.step) { + executionStepData.step = stepSerializer(executionStep.step); + } + + return executionStepData; +}; + +export default executionStepSerializer; diff --git a/packages/backend/src/serializers/execution-step.test.js b/packages/backend/src/serializers/execution-step.test.js new file mode 100644 index 00000000..037ccbcd --- /dev/null +++ b/packages/backend/src/serializers/execution-step.test.js @@ -0,0 +1,43 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import executionStepSerializer from './execution-step'; +import stepSerializer from './step'; +import { createExecutionStep } from '../../test/factories/execution-step'; +import { createStep } from '../../test/factories/step'; + +describe('executionStepSerializer', () => { + let executionStep, step; + + beforeEach(async () => { + step = await createStep(); + + executionStep = await createExecutionStep({ + stepId: step.id, + }); + }); + + it('should return the execution step data', async () => { + const expectedPayload = { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + }; + + expect(executionStepSerializer(executionStep)).toEqual(expectedPayload); + }); + + it('should return the execution step data with the step', async () => { + executionStep.step = step; + + const expectedPayload = { + step: stepSerializer(step), + }; + + expect(executionStepSerializer(executionStep)).toMatchObject( + expectedPayload + ); + }); +}); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 4e42b160..c35be112 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -10,6 +10,7 @@ import authSerializer from './auth.js'; import triggerSerializer from './trigger.js'; import actionSerializer from './action.js'; import executionSerializer from './execution.js'; +import executionStepSerializer from './execution-step.js'; const serializers = { User: userSerializer, @@ -24,6 +25,7 @@ const serializers = { Trigger: triggerSerializer, Action: actionSerializer, Execution: executionSerializer, + ExecutionStep: executionStepSerializer, }; export default serializers; diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js new file mode 100644 index 00000000..f7b50194 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js @@ -0,0 +1,39 @@ +const getExecutionStepsMock = async (executionSteps, steps) => { + const data = executionSteps.map((executionStep) => { + const step = steps.find((step) => step.id === executionStep.stepId); + + return { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + step: { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }, + }; + }); + + return { + data: data, + meta: { + count: executionSteps.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'ExecutionStep', + }, + }; +}; + +export default getExecutionStepsMock;