From 6027cb7cb0cca7655c1dbbf9d42236379d06fb87 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 10 Mar 2024 16:11:07 +0100 Subject: [PATCH] feat: Implement get connection flows API endpoint --- .../api/v1/connections/get-flows.js | 20 +++ .../api/v1/connections/get-flows.test.js | 128 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/helpers/renderer.js | 2 +- .../backend/src/routes/api/v1/connections.js | 16 +++ packages/backend/src/routes/index.js | 2 + 6 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/controllers/api/v1/connections/get-flows.js create mode 100644 packages/backend/src/controllers/api/v1/connections/get-flows.test.js create mode 100644 packages/backend/src/routes/api/v1/connections.js diff --git a/packages/backend/src/controllers/api/v1/connections/get-flows.js b/packages/backend/src/controllers/api/v1/connections/get-flows.js new file mode 100644 index 00000000..ade34b23 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/get-flows.js @@ -0,0 +1,20 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .joinRelated({ + steps: true, + }) + .withGraphFetched({ + steps: true, + }) + .where('steps.connection_id', request.params.connectionId) + .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/connections/get-flows.test.js b/packages/backend/src/controllers/api/v1/connections/get-flows.test.js new file mode 100644 index 00000000..3cfa6d73 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/get-flows.test.js @@ -0,0 +1,128 @@ +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.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; + +describe('GET /api/v1/connections/:connectionId/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 specified connection for current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const currentUserConnection = await createConnection({ + userId: currentUser.id, + key: 'webhook', + }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + connectionId: currentUserConnection.id, + }); + + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const currentUserFlowTwo = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + 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/connections/${currentUserConnection.id}/flows`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flows data of specified connection for another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const anotherUserConnection = await createConnection({ + userId: anotherUser.id, + key: 'webhook', + }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + connectionId: anotherUserConnection.id, + }); + + const actionStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'action', + }); + + const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id }); + + await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + 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/connections/${anotherUserConnection.id}/flows`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index f09e6a67..f294cba8 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -15,6 +15,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/connections/:connectionId/flows': { + action: 'read', + subject: 'Flow', + }, 'GET /api/v1/apps/:appKey/flows': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/helpers/renderer.js b/packages/backend/src/helpers/renderer.js index 94e1682d..91ea259a 100644 --- a/packages/backend/src/helpers/renderer.js +++ b/packages/backend/src/helpers/renderer.js @@ -15,7 +15,7 @@ const renderObject = (response, object, options) => { let data = isPaginated(object) ? object.records : object; const type = isPaginated(object) - ? object.records[0].constructor.name + ? object.records[0]?.constructor?.name || 'Object' : Array.isArray(object) ? object?.[0]?.constructor?.name || 'Object' : object.constructor.name; diff --git a/packages/backend/src/routes/api/v1/connections.js b/packages/backend/src/routes/api/v1/connections.js new file mode 100644 index 00000000..67f2f87e --- /dev/null +++ b/packages/backend/src/routes/api/v1/connections.js @@ -0,0 +1,16 @@ +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/connections/get-flows.js'; + +const router = Router(); + +router.get( + '/:connectionId/flows', + authenticateUser, + authorizeUser, + asyncHandler(getFlowsAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 6af8d140..a51700ac 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -10,6 +10,7 @@ import appAuthClientsRouter from './api/v1/app-auth-clients.js'; import appConfigsRouter from './api/v1/app-configs.ee.js'; import flowsRouter from './api/v1/flows.js'; import appsRouter from './api/v1/apps.js'; +import connectionsRouter from './api/v1/connections.js'; import executionsRouter from './api/v1/executions.js'; import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; @@ -30,6 +31,7 @@ router.use('/api/v1/app-auth-clients', appAuthClientsRouter); router.use('/api/v1/app-configs', appConfigsRouter); router.use('/api/v1/flows', flowsRouter); router.use('/api/v1/apps', appsRouter); +router.use('/api/v1/connections', connectionsRouter); router.use('/api/v1/executions', executionsRouter); router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/admin/roles', rolesRouter);