From 22ce29e86c93f48cc148b9f1fb966fa6ef51d510 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sat, 9 Mar 2024 15:09:12 +0100 Subject: [PATCH] feat: Implement API endpoint to get flows of the specified app --- .../src/controllers/api/v1/apps/get-flows.js | 23 ++++ .../controllers/api/v1/apps/get-flows.test.js | 129 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/routes/api/v1/apps.js | 9 ++ 4 files changed, 165 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/apps/get-flows.js create mode 100644 packages/backend/src/controllers/api/v1/apps/get-flows.test.js diff --git a/packages/backend/src/controllers/api/v1/apps/get-flows.js b/packages/backend/src/controllers/api/v1/apps/get-flows.js new file mode 100644 index 00000000..6554365e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-flows.js @@ -0,0 +1,23 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import App from '../../../../models/app.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .joinRelated({ + steps: true, + }) + .withGraphFetched({ + steps: true, + }) + .where('steps.app_key', app.key) + .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/apps/get-flows.test.js b/packages/backend/src/controllers/api/v1/apps/get-flows.test.js new file mode 100644 index 00000000..e5280415 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-flows.test.js @@ -0,0 +1,129 @@ +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 { 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/apps/:appKey/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 app for current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + }); + + 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/apps/webhook/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 app for another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + }); + + 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/apps/webhook/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .get('/api/v1/apps/invalid-app-key/flows') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index b9860b90..f09e6a67 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/apps/:appKey/flows': { + action: 'read', + subject: 'Flow', + }, 'GET /api/v1/executions/:executionId': { action: 'read', subject: 'Execution', diff --git a/packages/backend/src/routes/api/v1/apps.js b/packages/backend/src/routes/api/v1/apps.js index 0167032e..65145714 100644 --- a/packages/backend/src/routes/api/v1/apps.js +++ b/packages/backend/src/routes/api/v1/apps.js @@ -1,6 +1,7 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js'; import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js'; @@ -8,6 +9,7 @@ import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js' import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js'; import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js'; import getActionSubstepsAction from '../../../controllers/api/v1/apps/get-action-substeps.js'; +import getFlowsAction from '../../../controllers/api/v1/apps/get-flows.js'; const router = Router(); @@ -39,4 +41,11 @@ router.get( asyncHandler(getActionSubstepsAction) ); +router.get( + '/:appKey/flows', + authenticateUser, + authorizeUser, + asyncHandler(getFlowsAction) +); + export default router;