From 3ff89a03ac52e5df7c144e50f8901b555b01181f Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 6 Mar 2024 14:46:19 +0100 Subject: [PATCH] feat: Implement get executions API endpoint --- .../api/v1/executions/get-executions.js | 26 ++++ .../api/v1/executions/get-executions.test.js | 113 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + .../backend/src/routes/api/v1/executions.js | 8 ++ .../rest/api/v1/executions/get-executions.js | 39 ++++++ 5 files changed, 190 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/executions/get-executions.js create mode 100644 packages/backend/src/controllers/api/v1/executions/get-executions.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/executions/get-executions.js diff --git a/packages/backend/src/controllers/api/v1/executions/get-executions.js b/packages/backend/src/controllers/api/v1/executions/get-executions.js new file mode 100644 index 00000000..44c722f5 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-executions.js @@ -0,0 +1,26 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const executionsQuery = request.currentUser.authorizedExecutions + .withSoftDeleted() + .orderBy('created_at', 'desc') + .withGraphFetched({ + flow: { + steps: true, + }, + }); + + const executions = await paginateRest(executionsQuery, request.query.page); + + for (const execution of executions.records) { + const executionSteps = await execution.$relatedQuery('executionSteps'); + const status = executionSteps.some((step) => step.status === 'failure') + ? 'failure' + : 'success'; + + execution.status = status; + } + + renderObject(response, executions); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-executions.test.js b/packages/backend/src/controllers/api/v1/executions/get-executions.test.js new file mode 100644 index 00000000..585ffab3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-executions.test.js @@ -0,0 +1,113 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +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 { createPermission } from '../../../../../test/factories/permission'; +import getExecutionsMock from '../../../../../test/mocks/rest/api/v1/executions/get-executions'; + +describe('GET /api/v1/executions', () => { + 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 executions of current user', 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 currentUserExecutionOne = await createExecution({ + flowId: currentUserFlow.id, + }); + + const currentUserExecutionTwo = await createExecution({ + flowId: currentUserFlow.id, + deletedAt: new Date().toISOString(), + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/executions') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionsMock( + [currentUserExecutionTwo, currentUserExecutionOne], + currentUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the executions of another user', 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 anotherUserExecutionOne = await createExecution({ + flowId: anotherUserFlow.id, + }); + + const anotherUserExecutionTwo = await createExecution({ + flowId: anotherUserFlow.id, + deletedAt: new Date().toISOString(), + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/executions') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionsMock( + [anotherUserExecutionTwo, anotherUserExecutionOne], + anotherUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index e6d09d5a..07331f30 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -15,6 +15,10 @@ const authorizationList = { action: 'read', subject: 'Execution', }, + 'GET /api/v1/executions/': { + 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 8deee5a9..f62daa5c 100644 --- a/packages/backend/src/routes/api/v1/executions.js +++ b/packages/backend/src/routes/api/v1/executions.js @@ -2,10 +2,18 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; 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'; const router = Router(); +router.get( + '/', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionsAction) +); + router.get( '/:executionId', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js new file mode 100644 index 00000000..3620b4a2 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js @@ -0,0 +1,39 @@ +const getExecutionsMock = async (executions, flow, steps) => { + const data = executions.map((execution) => ({ + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + status: 'success', + flow: { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.active ? 'published' : 'draft', + steps: steps.map((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: executions.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'Execution', + }, + }; +}; + +export default getExecutionsMock;