diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution.js b/packages/backend/src/controllers/api/v1/executions/get-execution.js index c3b0d115..fe6d599c 100644 --- a/packages/backend/src/controllers/api/v1/executions/get-execution.js +++ b/packages/backend/src/controllers/api/v1/executions/get-execution.js @@ -2,6 +2,7 @@ import { renderObject } from '../../../../helpers/renderer.js'; export default async (request, response) => { const execution = await request.currentUser.authorizedExecutions + .clone() .withGraphFetched({ flow: { steps: true, diff --git a/packages/backend/src/controllers/api/v1/executions/get-executions.js b/packages/backend/src/controllers/api/v1/executions/get-executions.js index 44c722f5..bb4ca70e 100644 --- a/packages/backend/src/controllers/api/v1/executions/get-executions.js +++ b/packages/backend/src/controllers/api/v1/executions/get-executions.js @@ -3,6 +3,7 @@ import paginateRest from '../../../../helpers/pagination-rest.js'; export default async (request, response) => { const executionsQuery = request.currentUser.authorizedExecutions + .clone() .withSoftDeleted() .orderBy('created_at', 'desc') .withGraphFetched({ diff --git a/packages/backend/src/controllers/api/v1/flows/get-flow.js b/packages/backend/src/controllers/api/v1/flows/get-flow.js index f7ab107e..00474696 100644 --- a/packages/backend/src/controllers/api/v1/flows/get-flow.js +++ b/packages/backend/src/controllers/api/v1/flows/get-flow.js @@ -2,6 +2,7 @@ import { renderObject } from '../../../../helpers/renderer.js'; export default async (request, response) => { const flow = await request.currentUser.authorizedFlows + .clone() .withGraphJoined({ steps: true }) .orderBy('steps.position', 'asc') .findOne({ 'flows.id': request.params.flowId }) diff --git a/packages/backend/src/controllers/api/v1/flows/get-flows.js b/packages/backend/src/controllers/api/v1/flows/get-flows.js new file mode 100644 index 00000000..92e79fbe --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.js @@ -0,0 +1,21 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .withGraphFetched({ + steps: true, + }) + .where((builder) => { + if (request.query.name) { + builder.where('flows.name', 'ilike', `%${request.query.name}%`); + } + }) + .orderBy('active', 'desc') + .orderBy('updated_at', 'desc'); + + const flows = await paginateRest(flowsQuery, request.query.page); + + renderObject(response, flows); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/get-flows.test.js b/packages/backend/src/controllers/api/v1/flows/get-flows.test.js new file mode 100644 index 00000000..05839619 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.test.js @@ -0,0 +1,118 @@ +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'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; + +describe('GET /api/v1/flows', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flows data of current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + }); + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const currentUserFlowTwo = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + }); + const actionStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowTwo, currentUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + ] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flows data of another user', async () => { + const anotherUser = await createUser(); + + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + }); + const actionStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'action', + }); + + const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowTwo = await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'trigger', + }); + const actionStepFlowTwo = await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowTwo, anotherUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + ] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 28b719a2..b9860b90 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -11,6 +11,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/flows/': { + action: 'read', + subject: 'Flow', + }, 'GET /api/v1/executions/:executionId': { action: 'read', subject: 'Execution', diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js index 8147cf68..955d638e 100644 --- a/packages/backend/src/routes/api/v1/flows.js +++ b/packages/backend/src/routes/api/v1/flows.js @@ -2,10 +2,13 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; +import getFlowsAction from '../../../controllers/api/v1/flows/get-flows.js'; import getFlowAction from '../../../controllers/api/v1/flows/get-flow.js'; const router = Router(); +router.get('/', authenticateUser, authorizeUser, asyncHandler(getFlowsAction)); + router.get( '/:flowId', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js new file mode 100644 index 00000000..321db577 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js @@ -0,0 +1,36 @@ +const getFlowsMock = async (flows, steps) => { + const data = flows.map((flow) => { + const flowSteps = steps.filter((step) => step.flowId === flow.id); + + return { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + steps: flowSteps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + webhookUrl: step.webhookUrl, + })), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'Flow', + }, + }; +}; + +export default getFlowsMock;