diff --git a/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js new file mode 100644 index 0000000000..e9e865a22d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js @@ -0,0 +1,27 @@ +import { ref } from 'objection'; +import ExecutionStep from '../../../../models/execution-step.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .clone() + .findOne({ 'steps.id': request.params.stepId }) + .throwIfNotFound(); + + const previousSteps = await request.currentUser.authorizedSteps + .clone() + .withGraphJoined('executionSteps') + .where('flow_id', '=', step.flowId) + .andWhere('position', '<', step.position) + .andWhere( + 'executionSteps.created_at', + '=', + ExecutionStep.query() + .max('created_at') + .where('step_id', '=', ref('steps.id')) + .andWhere('status', 'success') + ) + .orderBy('steps.position', 'asc'); + + renderObject(response, previousSteps); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js new file mode 100644 index 0000000000..7e015e89f9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js @@ -0,0 +1,173 @@ +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'; +import { createStep } from '../../../../../test/factories/step'; +import { createExecutionStep } from '../../../../../test/factories/execution-step.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps'; + +describe('GET /api/v1/steps/:stepId/previous-steps', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the previous steps of the specified step of the current user', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const triggerStep = await createStep({ + flowId: currentUserflow.id, + type: 'trigger', + }); + + const actionStepOne = await createStep({ + flowId: currentUserflow.id, + type: 'action', + }); + + const actionStepTwo = await createStep({ + flowId: currentUserflow.id, + type: 'action', + }); + + const executionStepOne = await createExecutionStep({ + stepId: triggerStep.id, + }); + + const executionStepTwo = await createExecutionStep({ + stepId: actionStepOne.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getPreviousStepsMock( + [triggerStep, actionStepOne], + [executionStepOne, executionStepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the previous steps of the specified step of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const actionStepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const actionStepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const executionStepOne = await createExecutionStep({ + stepId: triggerStep.id, + }); + + const executionStepTwo = await createExecutionStep({ + stepId: actionStepOne.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getPreviousStepsMock( + [triggerStep, actionStepOne], + [executionStepOne, executionStepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + 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 notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid 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) + .get('/api/v1/steps/invalidFlowUUID/previous-steps') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 1822c1e042..e1cd177159 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -19,6 +19,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/steps/:stepId/previous-steps': { + action: 'update', + subject: 'Flow', + }, 'GET /api/v1/connections/:connectionId/flows': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js index 80fc2f0b58..36dad046d2 100644 --- a/packages/backend/src/routes/api/v1/steps.js +++ b/packages/backend/src/routes/api/v1/steps.js @@ -3,6 +3,7 @@ import asyncHandler from 'express-async-handler'; import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; +import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; const router = Router(); @@ -13,4 +14,11 @@ router.get( asyncHandler(getConnectionAction) ); +router.get( + '/:stepId/previous-steps', + authenticateUser, + authorizeUser, + asyncHandler(getPreviousStepsAction) +); + export default router; diff --git a/packages/backend/src/serializers/step.js b/packages/backend/src/serializers/step.js index 8f40856ff3..27a9060cf2 100644 --- a/packages/backend/src/serializers/step.js +++ b/packages/backend/src/serializers/step.js @@ -1,5 +1,7 @@ +import executionStepSerializer from './execution-step.js'; + const stepSerializer = (step) => { - return { + let stepData = { id: step.id, type: step.type, key: step.key, @@ -10,6 +12,14 @@ const stepSerializer = (step) => { position: step.position, parameters: step.parameters, }; + + if (step.executionSteps?.length > 0) { + stepData.executionSteps = step.executionSteps.map((executionStep) => + executionStepSerializer(executionStep) + ); + } + + return stepData; }; export default stepSerializer; diff --git a/packages/backend/src/serializers/step.test.js b/packages/backend/src/serializers/step.test.js index 4ddf918ca6..5ebc340e1d 100644 --- a/packages/backend/src/serializers/step.test.js +++ b/packages/backend/src/serializers/step.test.js @@ -1,6 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { createStep } from '../../test/factories/step'; +import { createExecutionStep } from '../../test/factories/execution-step'; import stepSerializer from './step'; +import executionStepSerializer from './execution-step'; describe('stepSerializer', () => { let step; @@ -24,4 +26,20 @@ describe('stepSerializer', () => { expect(stepSerializer(step)).toEqual(expectedPayload); }); + + it('should return step data with the execution steps', async () => { + const executionStepOne = await createExecutionStep({ stepId: step.id }); + const executionStepTwo = await createExecutionStep({ stepId: step.id }); + + step.executionSteps = [executionStepOne, executionStepTwo]; + + const expectedPayload = { + executionSteps: [ + executionStepSerializer(executionStepOne), + executionStepSerializer(executionStepTwo), + ], + }; + + expect(stepSerializer(step)).toMatchObject(expectedPayload); + }); }); diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js new file mode 100644 index 0000000000..7b5515edcb --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js @@ -0,0 +1,41 @@ +const getPreviousStepsMock = async (steps, executionSteps) => { + const data = steps.map((step) => { + const filteredExecutionSteps = executionSteps.filter( + (executionStep) => executionStep.stepId === step.id + ); + + return { + 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, + executionSteps: filteredExecutionSteps.map((executionStep) => ({ + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + })), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Step', + }, + }; +}; + +export default getPreviousStepsMock;