From 59b887079a511d6a3fcba9c919ae7859d3dcb683 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 31 Mar 2025 21:18:33 +0200 Subject: [PATCH] feat: Implement isOwner flag for get flows API endpoint --- .../controllers/api/v1/apps/get-flows.test.js | 2 +- .../api/v1/connections/get-flows.test.js | 2 +- .../api/v1/flows/get-flows.test.js | 15 ++++--- packages/backend/src/models/user.js | 2 + packages/backend/src/models/user.test.js | 17 ++++++++ packages/backend/src/serializers/flow.js | 4 ++ packages/backend/src/serializers/flow.test.js | 18 +++++++++ .../test/mocks/rest/api/v1/apps/get-flows.js | 39 +++++++++++++++++++ .../rest/api/v1/connections/get-flows.js | 39 +++++++++++++++++++ .../test/mocks/rest/api/v1/flows/get-flows.js | 3 +- 10 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 packages/backend/test/mocks/rest/api/v1/apps/get-flows.js create mode 100644 packages/backend/test/mocks/rest/api/v1/connections/get-flows.js 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 index 7f4ce775..d23667ca 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-flows.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-flows.test.js @@ -6,7 +6,7 @@ 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'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/apps/get-flows.js'; describe('GET /api/v1/apps/:appKey/flows', () => { let currentUser, currentUserRole, token; 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 index 1f596720..eb824a7e 100644 --- a/packages/backend/src/controllers/api/v1/connections/get-flows.test.js +++ b/packages/backend/src/controllers/api/v1/connections/get-flows.test.js @@ -7,7 +7,7 @@ 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'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/connections/get-flows.js'; describe('GET /api/v1/connections/:connectionId/flows', () => { let currentUser, currentUserRole, token; 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 07690534..fcd71c65 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 @@ -61,7 +61,8 @@ describe('GET /api/v1/flows', () => { actionStepFlowOne, triggerStepFlowTwo, actionStepFlowTwo, - ] + ], + currentUser.id ); expect(response.body).toStrictEqual(expectedPayload); @@ -111,7 +112,8 @@ describe('GET /api/v1/flows', () => { actionStepFlowOne, triggerStepFlowTwo, actionStepFlowTwo, - ] + ], + currentUser.id ); expect(response.body).toStrictEqual(expectedPayload); @@ -184,7 +186,8 @@ describe('GET /api/v1/flows', () => { actionStepFlowTwo, triggerStepFlowThree, actionStepFlowThree, - ] + ], + currentUser.id ); expect(response.body).toStrictEqual(expectedPayload); @@ -270,7 +273,8 @@ describe('GET /api/v1/flows', () => { actionStepFlowThree, triggerStepFlowFour, actionStepFlowFour, - ] + ], + currentUser.id ); expect(response.body).toStrictEqual(expectedPayload); @@ -356,7 +360,8 @@ describe('GET /api/v1/flows', () => { actionStepFlowOne, triggerStepFlowTwo, actionStepFlowTwo, - ] + ], + currentUser.id ); expect(response.body).toStrictEqual(expectedPayload); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 35d2c25e..eb8cd618 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -537,6 +537,8 @@ class User extends Base { .withGraphFetched({ steps: true, }) + .select('flows.*') + .select(Flow.raw('flows.user_id = ? as "isOwner"', [this.id])) .where((builder) => { if (name) { builder.where('flows.name', 'ilike', `%${name}%`); diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index 2b5ef387..82548f93 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -1333,6 +1333,23 @@ describe('User model', () => { ); }); + it('should return isOwner false if the flow is owned by another user', async () => { + const anotherUser = await createUser(); + + await createFlow({ + userId: anotherUser.id, + folderId: folder.id, + active: true, + name: 'Another User Flow One', + }); + + const flows = await currentUser.getFlows({ onlyOwnedFlows: false }, [ + folder.id, + ]); + + expect(flows[0].isOwner).toBe(false); + }); + it('should return specified flows with all filters together', async () => { const flows = await currentUser.getFlows( { diff --git a/packages/backend/src/serializers/flow.js b/packages/backend/src/serializers/flow.js index 28475644..0992ad3b 100644 --- a/packages/backend/src/serializers/flow.js +++ b/packages/backend/src/serializers/flow.js @@ -11,6 +11,10 @@ const flowSerializer = (flow) => { updatedAt: flow.updatedAt.getTime(), }; + if ('isOwner' in flow) { + flowData.isOwner = flow.isOwner; + } + if (flow.steps?.length > 0) { flowData.steps = flow.steps.map((step) => stepSerializer(step)); } diff --git a/packages/backend/src/serializers/flow.test.js b/packages/backend/src/serializers/flow.test.js index 6c185dd7..aabb13b8 100644 --- a/packages/backend/src/serializers/flow.test.js +++ b/packages/backend/src/serializers/flow.test.js @@ -43,4 +43,22 @@ describe('flowSerializer', () => { expect(flowSerializer(flow)).toMatchObject(expectedPayload); }); + + describe('isOwner', () => { + it('should not be defined by default', async () => { + expect(flowSerializer(flow)).not.toHaveProperty('isOwner'); + }); + + it('should return true if the flow is owned by the current user', async () => { + flow.isOwner = true; + + expect(flowSerializer(flow)).toMatchObject({ isOwner: true }); + }); + + it('should return false if the flow is owned by another user', async () => { + flow.isOwner = false; + + expect(flowSerializer(flow)).toMatchObject({ isOwner: false }); + }); + }); }); diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-flows.js b/packages/backend/test/mocks/rest/api/v1/apps/get-flows.js new file mode 100644 index 00000000..6012a6f6 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-flows.js @@ -0,0 +1,39 @@ +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', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: flowSteps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + name: step.name, + 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; diff --git a/packages/backend/test/mocks/rest/api/v1/connections/get-flows.js b/packages/backend/test/mocks/rest/api/v1/connections/get-flows.js new file mode 100644 index 00000000..6012a6f6 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/connections/get-flows.js @@ -0,0 +1,39 @@ +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', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: flowSteps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + name: step.name, + 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; 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 index 6012a6f6..c1294454 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js @@ -1,4 +1,4 @@ -const getFlowsMock = async (flows, steps) => { +const getFlowsMock = async (flows, steps, currentUserId) => { const data = flows.map((flow) => { const flowSteps = steps.filter((step) => step.flowId === flow.id); @@ -9,6 +9,7 @@ const getFlowsMock = async (flows, steps) => { status: flow.active ? 'published' : 'draft', createdAt: flow.createdAt.getTime(), updatedAt: flow.updatedAt.getTime(), + isOwner: flow.userId === currentUserId, steps: flowSteps.map((step) => ({ appKey: step.appKey, iconUrl: step.iconUrl,