From 434029ccb81b9d5e6468c361918ebb76189f73f6 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 5 Feb 2025 19:08:49 +0100 Subject: [PATCH] feat: Implement folder filters for get flows API endpoint --- .../src/controllers/api/v1/flows/get-flows.js | 18 +- .../api/v1/flows/get-flows.test.js | 246 ++++++++++++++++++ packages/backend/src/models/user.js | 29 +++ packages/backend/src/models/user.test.js | 111 +++++++- 4 files changed, 390 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/flows/get-flows.js b/packages/backend/src/controllers/api/v1/flows/get-flows.js index 92e79fbe..e4951cce 100644 --- a/packages/backend/src/controllers/api/v1/flows/get-flows.js +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.js @@ -2,20 +2,14 @@ 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'); + await request.currentUser.hasFolderAccess(request.body.folderId); + const flowsQuery = request.currentUser.getFlows(flowParams(request)); const flows = await paginateRest(flowsQuery, request.query.page); renderObject(response, flows); }; + +const flowParams = (request) => { + return { folderId: request.query.folderId, name: request.query.name }; +}; 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 index 3af8537d..07690534 100644 --- a/packages/backend/src/controllers/api/v1/flows/get-flows.test.js +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.test.js @@ -5,6 +5,7 @@ import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-us import { createUser } from '../../../../../test/factories/user'; import { createFlow } from '../../../../../test/factories/flow'; import { createStep } from '../../../../../test/factories/step'; +import { createFolder } from '../../../../../test/factories/folder'; import { createPermission } from '../../../../../test/factories/permission'; import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; @@ -115,4 +116,249 @@ describe('GET /api/v1/flows', () => { expect(response.body).toStrictEqual(expectedPayload); }); + + it('should return the all flows data of the current user', async () => { + const folderOne = await createFolder({ userId: currentUser.id }); + + const currentUserFlowOne = await createFlow({ + userId: currentUser.id, + folderId: folderOne.id, + }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + }); + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const folderTwo = await createFolder({ userId: currentUser.id }); + + const currentUserFlowTwo = await createFlow({ + userId: currentUser.id, + folderId: folderTwo.id, + }); + + const triggerStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + }); + + const actionStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + const currentUserFlowThree = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowThree = await createStep({ + flowId: currentUserFlowThree.id, + type: 'trigger', + }); + + const actionStepFlowThree = await createStep({ + flowId: currentUserFlowThree.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( + [currentUserFlowThree, currentUserFlowTwo, currentUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + triggerStepFlowThree, + actionStepFlowThree, + ] + ); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return the uncategorized flows data of the current user', async () => { + const folderOne = await createFolder({ userId: currentUser.id }); + + const currentUserFlowOne = await createFlow({ + userId: currentUser.id, + folderId: folderOne.id, + }); + + await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + }); + + await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const folderTwo = await createFolder({ userId: currentUser.id }); + + const currentUserFlowTwo = await createFlow({ + userId: currentUser.id, + folderId: folderTwo.id, + }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + const currentUserFlowThree = await createFlow({ + userId: currentUser.id, + }); + + const triggerStepFlowThree = await createStep({ + flowId: currentUserFlowThree.id, + type: 'trigger', + }); + + const actionStepFlowThree = await createStep({ + flowId: currentUserFlowThree.id, + type: 'action', + }); + + const currentUserFlowFour = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowFour = await createStep({ + flowId: currentUserFlowFour.id, + type: 'trigger', + }); + + const actionStepFlowFour = await createStep({ + flowId: currentUserFlowFour.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/flows?folderId=null`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowFour, currentUserFlowThree], + [ + triggerStepFlowThree, + actionStepFlowThree, + triggerStepFlowFour, + actionStepFlowFour, + ] + ); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return the all flows data of the current user for specified folder', async () => { + const folderOne = await createFolder({ userId: currentUser.id }); + + const currentUserFlowOne = await createFlow({ + userId: currentUser.id, + folderId: folderOne.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, + folderId: folderOne.id, + }); + + const triggerStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + }); + + const actionStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + const folderTwo = await createFolder({ userId: currentUser.id }); + + const currentUserFlowThree = await createFlow({ + userId: currentUser.id, + folderId: folderTwo.id, + }); + + await createStep({ + flowId: currentUserFlowThree.id, + type: 'trigger', + }); + + await createStep({ + flowId: currentUserFlowThree.id, + type: 'action', + }); + + const currentUserFlowFour = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: currentUserFlowFour.id, + type: 'trigger', + }); + + await createStep({ + flowId: currentUserFlowFour.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/flows?folderId=${folderOne.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowTwo, currentUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + ] + ); + + expect(response.body).toStrictEqual(expectedPayload); + }); }); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 1ed7ece5..cb1e2156 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -516,6 +516,35 @@ class User extends Base { return invoices; } + async hasFolderAccess(folderId) { + if (folderId && folderId !== 'null') { + await this.$relatedQuery('folders').findById(folderId).throwIfNotFound(); + } + + return true; + } + + getFlows({ folderId, name }) { + return this.authorizedFlows + .clone() + .withGraphFetched({ + steps: true, + }) + .where((builder) => { + if (name) { + builder.where('flows.name', 'ilike', `%${name}%`); + } + + if (folderId === 'null') { + builder.whereNull('flows.folder_id'); + } else if (folderId) { + builder.where('flows.folder_id', folderId); + } + }) + .orderBy('active', 'desc') + .orderBy('updated_at', 'desc'); + } + async getApps(name) { const connections = await this.authorizedConnections .clone() diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index a7920b98..4eae81c0 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -1,5 +1,6 @@ -import { describe, it, expect, vi } from 'vitest'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; import { DateTime, Duration } from 'luxon'; +import Crypto from 'crypto'; import appConfig from '../config/app.js'; import * as licenseModule from '../helpers/license.ee.js'; import Base from './base.js'; @@ -32,6 +33,7 @@ import { createStep } from '../../test/factories/step.js'; import { createExecution } from '../../test/factories/execution.js'; import { createSubscription } from '../../test/factories/subscription.js'; import { createUsageData } from '../../test/factories/usage-data.js'; +import { createFolder } from '../../test/factories/folder.js'; import Billing from '../helpers/billing/index.ee.js'; describe('User model', () => { @@ -1142,7 +1144,112 @@ describe('User model', () => { it('should return empty array without any subscriptions', async () => { const user = await createUser(); - expect(await user.getInvoices()).toStrictEqual([]); + expect(await user.getInvoices()).toEqual([]); + }); + }); + + describe('hasFolderAccess', () => { + let currentUser, currentUserFolder; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserFolder = await createFolder({ userId: currentUser.id }); + }); + + it('should return true if the user has access to the folder', async () => { + const hasAccess = await currentUser.hasFolderAccess(currentUserFolder.id); + expect(hasAccess).toBe(true); + }); + + it('should throw an error if the user does not have access to the folder', async () => { + const anotherUser = await createUser(); + const anotherUserFolder = await createFolder({ userId: anotherUser.id }); + + await expect( + currentUser.hasFolderAccess(anotherUserFolder.id) + ).rejects.toThrow(); + }); + + it('should throw an error if the folder does not exist', async () => { + const nonExistingFolderUUID = Crypto.randomUUID(); + + await expect( + currentUser.hasFolderAccess(nonExistingFolderUUID) + ).rejects.toThrow(); + }); + }); + + describe('getFlows', () => { + let currentUser, currentUserRole, folder, flowOne, flowTwo; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + folder = await createFolder({ userId: currentUser.id }); + + flowOne = await createFlow({ + userId: currentUser.id, + folderId: folder.id, + name: 'Flow One', + }); + + flowTwo = await createFlow({ + userId: currentUser.id, + name: 'Flow Two', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + currentUser = await currentUser.$query().withGraphFetched({ + role: true, + permissions: true, + }); + }); + + it('should return flows filtered by folderId', async () => { + const flows = await currentUser.getFlows({ folderId: folder.id }); + + expect(flows).toHaveLength(1); + expect(flows[0].id).toBe(flowOne.id); + }); + + it('should return flows filtered by name', async () => { + const flows = await currentUser.getFlows({ name: 'Flow Two' }); + + expect(flows).toHaveLength(1); + expect(flows[0].id).toBe(flowTwo.id); + }); + + it('should return flows filtered by folderId and name', async () => { + const flows = await currentUser.getFlows({ + folderId: folder.id, + name: 'Flow One', + }); + + expect(flows).toHaveLength(1); + expect(flows[0].id).toBe(flowOne.id); + }); + + it('should return all flows if no filters are provided', async () => { + const flows = await currentUser.getFlows({}); + + expect(flows).toHaveLength(2); + expect(flows.map((flow) => flow.id)).toEqual( + expect.arrayContaining([flowOne.id, flowTwo.id]) + ); + }); + + it('should return uncategorized flows if the folderId is null', async () => { + const flows = await currentUser.getFlows({ folderId: 'null' }); + + expect(flows).toHaveLength(1); + expect(flows[0].id).toBe(flowTwo.id); }); });