From 0d126a8e2b92f169d70ad3b9d3d26967a344c71b Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 11 Sep 2024 09:26:41 +0000 Subject: [PATCH 1/5] feat: write REST API endpoint to duplicate flow --- .../api/v1/flows/duplicate-flow.js | 11 + .../api/v1/flows/duplicate-flow.test.js | 330 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/models/flow.js | 81 +++++ packages/backend/src/routes/api/v1/flows.js | 7 + .../mocks/rest/api/v1/flows/duplicate-flow.js | 37 ++ 6 files changed, 470 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/flows/duplicate-flow.js create mode 100644 packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js diff --git a/packages/backend/src/controllers/api/v1/flows/duplicate-flow.js b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.js new file mode 100644 index 00000000..a4c7d582 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const flow = await request.currentUser.authorizedFlows + .findById(request.params.flowId) + .throwIfNotFound(); + + const duplicatedFlow = await flow.duplicateFor(request.currentUser); + + renderObject(response, duplicatedFlow, { status: 201 }); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js new file mode 100644 index 00000000..fa8a4c47 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js @@ -0,0 +1,330 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +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 duplicateFlowMock from '../../../../../test/mocks/rest/api/v1/flows/duplicate-flow.js'; + +describe('POST /api/v1/flows/:flowId/duplicate', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return duplicated flow data of current user', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + + const triggerStep = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + appKey: 'webhook', + key: 'catchRawWebhook', + }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + appKey: 'ntfy', + key: 'sendMessage', + parameters: { + topic: 'Test notification', + message: `Message: {{step.${triggerStep.id}.body.message}} by {{step.${triggerStep.id}.body.sender}}`, + }, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/flows/${currentUserFlow.id}/duplicate`) + .set('Authorization', token) + .expect(201); + + const refetchedDuplicateFlow = await currentUser + .$relatedQuery('flows') + .findById(response.body.data.id); + + const refetchedDuplicateFlowSteps = await refetchedDuplicateFlow + .$relatedQuery('steps') + .orderBy('position', 'asc'); + + const expectedPayload = await duplicateFlowMock( + refetchedDuplicateFlow, + refetchedDuplicateFlowSteps + ); + + const refetchedDuplicateFlowTriggerStep = refetchedDuplicateFlowSteps[0]; + const refetchedDuplicateFlowActionStep = refetchedDuplicateFlowSteps[1]; + + expect(response.body).toStrictEqual(expectedPayload); + + expect(refetchedDuplicateFlow.userId).toStrictEqual(currentUser.id); + + expect(refetchedDuplicateFlowSteps.length).toStrictEqual(2); + + expect(refetchedDuplicateFlowTriggerStep.appKey).toStrictEqual( + triggerStep.appKey + ); + + expect(refetchedDuplicateFlowTriggerStep.key).toStrictEqual( + triggerStep.key + ); + + expect(refetchedDuplicateFlowTriggerStep.connectionId).toStrictEqual( + triggerStep.connectionId + ); + + expect(refetchedDuplicateFlowTriggerStep.position).toStrictEqual( + triggerStep.position + ); + + expect(refetchedDuplicateFlowTriggerStep.parameters).toStrictEqual( + triggerStep.parameters + ); + + expect(refetchedDuplicateFlowTriggerStep.type).toStrictEqual( + triggerStep.type + ); + + expect(refetchedDuplicateFlowActionStep.appKey).toStrictEqual( + actionStep.appKey + ); + + expect(refetchedDuplicateFlowActionStep.key).toStrictEqual(actionStep.key); + + expect(refetchedDuplicateFlowActionStep.connectionId).toStrictEqual( + actionStep.connectionId + ); + + expect(refetchedDuplicateFlowActionStep.position).toStrictEqual( + actionStep.position + ); + + expect(refetchedDuplicateFlowActionStep.parameters.topic).toStrictEqual( + actionStep.parameters.topic + ); + + expect(refetchedDuplicateFlowActionStep.parameters.message).toStrictEqual( + actionStep.parameters.message.replaceAll( + `{{step.${triggerStep.id}.`, + `{{step.${refetchedDuplicateFlowTriggerStep.id}.` + ) + ); + + expect(refetchedDuplicateFlowActionStep.type).toStrictEqual( + actionStep.type + ); + + expect(refetchedDuplicateFlowTriggerStep.webhookPath).toStrictEqual( + `/webhooks/flows/${refetchedDuplicateFlow.id}` + ); + }); + + it('should return duplicated flow data of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + appKey: 'webhook', + key: 'catchRawWebhook', + }); + + const actionStep = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + appKey: 'ntfy', + key: 'sendMessage', + parameters: { + topic: 'Test notification', + message: `Message: {{step.${triggerStep.id}.body.message}} by {{step.${triggerStep.id}.body.sender}}`, + }, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/flows/${anotherUserFlow.id}/duplicate`) + .set('Authorization', token) + .expect(201); + + const refetchedDuplicateFlow = await currentUser + .$relatedQuery('flows') + .findById(response.body.data.id); + + const refetchedDuplicateFlowSteps = await refetchedDuplicateFlow + .$relatedQuery('steps') + .orderBy('position', 'asc'); + + const expectedPayload = await duplicateFlowMock( + refetchedDuplicateFlow, + refetchedDuplicateFlowSteps + ); + + const refetchedDuplicateFlowTriggerStep = refetchedDuplicateFlowSteps[0]; + const refetchedDuplicateFlowActionStep = refetchedDuplicateFlowSteps[1]; + + expect(response.body).toStrictEqual(expectedPayload); + + expect(refetchedDuplicateFlow.userId).toStrictEqual(currentUser.id); + + expect(refetchedDuplicateFlowSteps.length).toStrictEqual(2); + + expect(refetchedDuplicateFlowTriggerStep.appKey).toStrictEqual( + triggerStep.appKey + ); + + expect(refetchedDuplicateFlowTriggerStep.key).toStrictEqual( + triggerStep.key + ); + + expect(refetchedDuplicateFlowTriggerStep.connectionId).toStrictEqual( + triggerStep.connectionId + ); + + expect(refetchedDuplicateFlowTriggerStep.position).toStrictEqual( + triggerStep.position + ); + + expect(refetchedDuplicateFlowTriggerStep.parameters).toStrictEqual( + triggerStep.parameters + ); + + expect(refetchedDuplicateFlowTriggerStep.type).toStrictEqual( + triggerStep.type + ); + + expect(refetchedDuplicateFlowActionStep.appKey).toStrictEqual( + actionStep.appKey + ); + + expect(refetchedDuplicateFlowActionStep.key).toStrictEqual(actionStep.key); + + expect(refetchedDuplicateFlowActionStep.connectionId).toStrictEqual( + actionStep.connectionId + ); + + expect(refetchedDuplicateFlowActionStep.position).toStrictEqual( + actionStep.position + ); + + expect(refetchedDuplicateFlowActionStep.parameters.topic).toStrictEqual( + actionStep.parameters.topic + ); + + expect(refetchedDuplicateFlowActionStep.parameters.message).toStrictEqual( + actionStep.parameters.message.replaceAll( + `{{step.${triggerStep.id}.`, + `{{step.${refetchedDuplicateFlowTriggerStep.id}.` + ) + ); + + expect(refetchedDuplicateFlowActionStep.type).toStrictEqual( + actionStep.type + ); + + expect(refetchedDuplicateFlowTriggerStep.webhookPath).toStrictEqual( + `/webhooks/flows/${refetchedDuplicateFlow.id}` + ); + }); + + it('should return not found response for not existing flow UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .post(`/api/v1/flows/${notExistingFlowUUID}/duplicate`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for unauthorized flow', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post(`/api/v1/flows/${anotherUserFlow.id}/duplicate`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post('/api/v1/flows/invalidFlowUUID/duplicate') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 95cd3f0c..94e6bf08 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -101,6 +101,10 @@ const authorizationList = { action: 'update', subject: 'Flow', }, + 'POST /api/v1/flows/:flowId/duplicate': { + action: 'create', + subject: 'Flow', + }, 'POST /api/v1/flows/:flowId/steps': { action: 'update', subject: 'Flow', diff --git a/packages/backend/src/models/flow.js b/packages/backend/src/models/flow.js index 7806a9bb..1df12d52 100644 --- a/packages/backend/src/models/flow.js +++ b/packages/backend/src/models/flow.js @@ -196,6 +196,87 @@ class Flow extends Base { await this.$query().delete(); } + async duplicateFor(user) { + const steps = await this.$relatedQuery('steps').orderBy( + 'steps.position', + 'asc' + ); + + const duplicatedFlow = await user.$relatedQuery('flows').insertAndFetch({ + name: `Copy of ${this.name}`, + active: false, + }); + + const updateStepId = (value, newStepIds) => { + let newValue = value; + + const stepIdEntries = Object.entries(newStepIds); + for (const stepIdEntry of stepIdEntries) { + const [oldStepId, newStepId] = stepIdEntry; + + const partialOldVariable = `{{step.${oldStepId}.`; + const partialNewVariable = `{{step.${newStepId}.`; + + newValue = newValue.replaceAll(partialOldVariable, partialNewVariable); + } + + return newValue; + }; + + const updateStepVariables = (parameters, newStepIds) => { + const entries = Object.entries(parameters); + + return entries.reduce((result, [key, value]) => { + if (typeof value === 'string') { + return { + ...result, + [key]: updateStepId(value, newStepIds), + }; + } + + if (Array.isArray(value)) { + return { + ...result, + [key]: value.map((item) => updateStepVariables(item, newStepIds)), + }; + } + + return { + ...result, + [key]: value, + }; + }, {}); + }; + + const newStepIds = {}; + for (const step of steps) { + const duplicatedStep = await duplicatedFlow + .$relatedQuery('steps') + .insert({ + key: step.key, + appKey: step.appKey, + type: step.type, + connectionId: step.connectionId, + position: step.position, + parameters: updateStepVariables(step.parameters, newStepIds), + }); + + if (duplicatedStep.isTrigger) { + await duplicatedStep.updateWebhookUrl(); + } + + newStepIds[step.id] = duplicatedStep.id; + } + + const duplicatedFlowWithSteps = duplicatedFlow + .$query() + .withGraphJoined({ steps: true }) + .orderBy('steps.position', 'asc') + .throwIfNotFound(); + + return duplicatedFlowWithSteps; + } + async $beforeUpdate(opt, queryContext) { await super.$beforeUpdate(opt, queryContext); diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js index 9f11b30c..83d2c021 100644 --- a/packages/backend/src/routes/api/v1/flows.js +++ b/packages/backend/src/routes/api/v1/flows.js @@ -7,6 +7,7 @@ import updateFlowAction from '../../../controllers/api/v1/flows/update-flow.js'; import createFlowAction from '../../../controllers/api/v1/flows/create-flow.js'; import createStepAction from '../../../controllers/api/v1/flows/create-step.js'; import deleteFlowAction from '../../../controllers/api/v1/flows/delete-flow.js'; +import duplicateFlowAction from '../../../controllers/api/v1/flows/duplicate-flow.js'; const router = Router(); @@ -21,6 +22,12 @@ router.post( authorizeUser, createStepAction ); +router.post( + '/:flowId/duplicate', + authenticateUser, + authorizeUser, + duplicateFlowAction +); router.delete('/:flowId', authenticateUser, authorizeUser, deleteFlowAction); diff --git a/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js new file mode 100644 index 00000000..db1e4a47 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js @@ -0,0 +1,37 @@ +const getFlowMock = async (flow, steps = []) => { + const data = { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + }; + + if (steps.length) { + data.steps = steps.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: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Flow', + }, + }; +}; + +export default getFlowMock; From 22299868fa109e06a9a50672893ed58b6629526a Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 11 Sep 2024 09:26:57 +0000 Subject: [PATCH 2/5] feat: use REST API endpoint to duplicate flow --- .../src/components/FlowContextMenu/index.jsx | 17 ++++++++++---- packages/web/src/hooks/useDuplicateFlow.js | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/hooks/useDuplicateFlow.js diff --git a/packages/web/src/components/FlowContextMenu/index.jsx b/packages/web/src/components/FlowContextMenu/index.jsx index bba8f10b..a484162a 100644 --- a/packages/web/src/components/FlowContextMenu/index.jsx +++ b/packages/web/src/components/FlowContextMenu/index.jsx @@ -13,6 +13,7 @@ import * as URLS from 'config/urls'; import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow'; import useFormatMessage from 'hooks/useFormatMessage'; +import useDuplicateFlow from 'hooks/useDuplicateFlow'; function ContextMenu(props) { const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow, appKey } = @@ -20,13 +21,11 @@ function ContextMenu(props) { const enqueueSnackbar = useEnqueueSnackbar(); const formatMessage = useFormatMessage(); const queryClient = useQueryClient(); - const [duplicateFlow] = useMutation(DUPLICATE_FLOW); + const { mutateAsync: duplicateFlow } = useDuplicateFlow(flowId); const [deleteFlow] = useMutation(DELETE_FLOW); const onFlowDuplicate = React.useCallback(async () => { - await duplicateFlow({ - variables: { input: { id: flowId } }, - }); + await duplicateFlow(); if (appKey) { await queryClient.invalidateQueries({ @@ -43,7 +42,15 @@ function ContextMenu(props) { onDuplicateFlow?.(); onClose(); - }, [flowId, onClose, duplicateFlow, queryClient, onDuplicateFlow]); + }, [ + appKey, + enqueueSnackbar, + onClose, + duplicateFlow, + queryClient, + onDuplicateFlow, + formatMessage, + ]); const onFlowDelete = React.useCallback(async () => { await deleteFlow({ diff --git a/packages/web/src/hooks/useDuplicateFlow.js b/packages/web/src/hooks/useDuplicateFlow.js new file mode 100644 index 00000000..c8df605f --- /dev/null +++ b/packages/web/src/hooks/useDuplicateFlow.js @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useDuplicateFlow(flowId) { + const queryClient = useQueryClient(); + + const query = useMutation({ + mutationFn: async () => { + const { data } = await api.post(`/v1/flows/${flowId}/duplicate`); + + return data; + }, + + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['flows'], + }); + }, + }); + + return query; +} From 8352540fcb420774d955d6d59addd8d8cf01d198 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 11 Sep 2024 09:28:55 +0000 Subject: [PATCH 3/5] chore: remove redundant duplicate flow mutation --- .../backend/src/graphql/mutation-resolvers.js | 2 - .../src/graphql/mutations/duplicate-flow.js | 78 ------------------- packages/backend/src/graphql/schema.graphql | 5 -- .../src/components/FlowContextMenu/index.jsx | 1 - .../src/graphql/mutations/duplicate-flow.js | 27 ------- 5 files changed, 113 deletions(-) delete mode 100644 packages/backend/src/graphql/mutations/duplicate-flow.js delete mode 100644 packages/web/src/graphql/mutations/duplicate-flow.js diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 76811fd8..23db05be 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -1,4 +1,3 @@ -import duplicateFlow from './mutations/duplicate-flow.js'; import updateFlowStatus from './mutations/update-flow-status.js'; import updateStep from './mutations/update-step.js'; @@ -24,7 +23,6 @@ const mutationResolvers = { deleteCurrentUser, deleteFlow, deleteStep, - duplicateFlow, executeFlow, generateAuthUrl, resetConnection, diff --git a/packages/backend/src/graphql/mutations/duplicate-flow.js b/packages/backend/src/graphql/mutations/duplicate-flow.js deleted file mode 100644 index 4b5c67f0..00000000 --- a/packages/backend/src/graphql/mutations/duplicate-flow.js +++ /dev/null @@ -1,78 +0,0 @@ -function updateStepId(value, newStepIds) { - let newValue = value; - - const stepIdEntries = Object.entries(newStepIds); - for (const stepIdEntry of stepIdEntries) { - const [oldStepId, newStepId] = stepIdEntry; - const partialOldVariable = `{{step.${oldStepId}.`; - const partialNewVariable = `{{step.${newStepId}.`; - - newValue = newValue.replace(partialOldVariable, partialNewVariable); - } - - return newValue; -} - -function updateStepVariables(parameters, newStepIds) { - const entries = Object.entries(parameters); - return entries.reduce((result, [key, value]) => { - if (typeof value === 'string') { - return { - ...result, - [key]: updateStepId(value, newStepIds), - }; - } - - if (Array.isArray(value)) { - return { - ...result, - [key]: value.map((item) => updateStepVariables(item, newStepIds)), - }; - } - - return { - ...result, - [key]: value, - }; - }, {}); -} - -const duplicateFlow = async (_parent, params, context) => { - context.currentUser.can('create', 'Flow'); - - const flow = await context.currentUser - .authorizedFlows - .withGraphJoined('[steps]') - .orderBy('steps.position', 'asc') - .findOne({ 'flows.id': params.input.id }) - .throwIfNotFound(); - - const duplicatedFlow = await context.currentUser - .$relatedQuery('flows') - .insert({ - name: `Copy of ${flow.name}`, - active: false, - }); - - const newStepIds = {}; - for (const step of flow.steps) { - const duplicatedStep = await duplicatedFlow.$relatedQuery('steps').insert({ - key: step.key, - appKey: step.appKey, - type: step.type, - connectionId: step.connectionId, - position: step.position, - parameters: updateStepVariables(step.parameters, newStepIds), - }); - - if (duplicatedStep.isTrigger) { - await duplicatedStep.updateWebhookUrl(); - } - - newStepIds[step.id] = duplicatedStep.id; - } - - return duplicatedFlow; -}; - -export default duplicateFlow; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 5b4520b5..c960eb42 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -8,7 +8,6 @@ type Mutation { deleteCurrentUser: Boolean deleteFlow(input: DeleteFlowInput): Boolean deleteStep(input: DeleteStepInput): Step - duplicateFlow(input: DuplicateFlowInput): Flow executeFlow(input: ExecuteFlowInput): executeFlowType generateAuthUrl(input: GenerateAuthUrlInput): AuthLink resetConnection(input: ResetConnectionInput): Connection @@ -259,10 +258,6 @@ input DeleteFlowInput { id: String! } -input DuplicateFlowInput { - id: String! -} - input UpdateStepInput { id: String previousStepId: String diff --git a/packages/web/src/components/FlowContextMenu/index.jsx b/packages/web/src/components/FlowContextMenu/index.jsx index a484162a..c8413508 100644 --- a/packages/web/src/components/FlowContextMenu/index.jsx +++ b/packages/web/src/components/FlowContextMenu/index.jsx @@ -11,7 +11,6 @@ import { Link } from 'react-router-dom'; import Can from 'components/Can'; import * as URLS from 'config/urls'; import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; -import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow'; import useFormatMessage from 'hooks/useFormatMessage'; import useDuplicateFlow from 'hooks/useDuplicateFlow'; diff --git a/packages/web/src/graphql/mutations/duplicate-flow.js b/packages/web/src/graphql/mutations/duplicate-flow.js deleted file mode 100644 index 6e359ff5..00000000 --- a/packages/web/src/graphql/mutations/duplicate-flow.js +++ /dev/null @@ -1,27 +0,0 @@ -import { gql } from '@apollo/client'; -export const DUPLICATE_FLOW = gql` - mutation DuplicateFlow($input: DuplicateFlowInput) { - duplicateFlow(input: $input) { - id - name - active - status - steps { - id - type - key - appKey - iconUrl - webhookUrl - status - position - connection { - id - verified - createdAt - } - parameters - } - } - } -`; From c6003b669547d04f0ff0bbb82c4a37d3f2e9fbdc Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 17 Sep 2024 13:05:23 +0300 Subject: [PATCH 4/5] fix: Correct duplicate flow mock name --- .../backend/test/mocks/rest/api/v1/flows/duplicate-flow.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js index db1e4a47..67684191 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js @@ -1,4 +1,4 @@ -const getFlowMock = async (flow, steps = []) => { +const duplicateFlowMock = async (flow, steps = []) => { const data = { active: flow.active, id: flow.id, @@ -34,4 +34,4 @@ const getFlowMock = async (flow, steps = []) => { }; }; -export default getFlowMock; +export default duplicateFlowMock; From bf6ff6b0f73eace4ad927e3aedfc81b5649d312b Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 17 Sep 2024 13:18:48 +0300 Subject: [PATCH 5/5] refactor: Revise duplicate flow controller tests --- .../api/v1/flows/duplicate-flow.test.js | 130 +----------------- 1 file changed, 2 insertions(+), 128 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js index fa8a4c47..924b4f10 100644 --- a/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js +++ b/packages/backend/src/controllers/api/v1/flows/duplicate-flow.test.js @@ -29,7 +29,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => { key: 'catchRawWebhook', }); - const actionStep = await createStep({ + await createStep({ flowId: currentUserFlow.id, type: 'action', appKey: 'ntfy', @@ -72,71 +72,8 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => { refetchedDuplicateFlowSteps ); - const refetchedDuplicateFlowTriggerStep = refetchedDuplicateFlowSteps[0]; - const refetchedDuplicateFlowActionStep = refetchedDuplicateFlowSteps[1]; - expect(response.body).toStrictEqual(expectedPayload); - expect(refetchedDuplicateFlow.userId).toStrictEqual(currentUser.id); - - expect(refetchedDuplicateFlowSteps.length).toStrictEqual(2); - - expect(refetchedDuplicateFlowTriggerStep.appKey).toStrictEqual( - triggerStep.appKey - ); - - expect(refetchedDuplicateFlowTriggerStep.key).toStrictEqual( - triggerStep.key - ); - - expect(refetchedDuplicateFlowTriggerStep.connectionId).toStrictEqual( - triggerStep.connectionId - ); - - expect(refetchedDuplicateFlowTriggerStep.position).toStrictEqual( - triggerStep.position - ); - - expect(refetchedDuplicateFlowTriggerStep.parameters).toStrictEqual( - triggerStep.parameters - ); - - expect(refetchedDuplicateFlowTriggerStep.type).toStrictEqual( - triggerStep.type - ); - - expect(refetchedDuplicateFlowActionStep.appKey).toStrictEqual( - actionStep.appKey - ); - - expect(refetchedDuplicateFlowActionStep.key).toStrictEqual(actionStep.key); - - expect(refetchedDuplicateFlowActionStep.connectionId).toStrictEqual( - actionStep.connectionId - ); - - expect(refetchedDuplicateFlowActionStep.position).toStrictEqual( - actionStep.position - ); - - expect(refetchedDuplicateFlowActionStep.parameters.topic).toStrictEqual( - actionStep.parameters.topic - ); - - expect(refetchedDuplicateFlowActionStep.parameters.message).toStrictEqual( - actionStep.parameters.message.replaceAll( - `{{step.${triggerStep.id}.`, - `{{step.${refetchedDuplicateFlowTriggerStep.id}.` - ) - ); - - expect(refetchedDuplicateFlowActionStep.type).toStrictEqual( - actionStep.type - ); - - expect(refetchedDuplicateFlowTriggerStep.webhookPath).toStrictEqual( - `/webhooks/flows/${refetchedDuplicateFlow.id}` - ); }); it('should return duplicated flow data of another user', async () => { @@ -150,7 +87,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => { key: 'catchRawWebhook', }); - const actionStep = await createStep({ + await createStep({ flowId: anotherUserFlow.id, type: 'action', appKey: 'ntfy', @@ -193,71 +130,8 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => { refetchedDuplicateFlowSteps ); - const refetchedDuplicateFlowTriggerStep = refetchedDuplicateFlowSteps[0]; - const refetchedDuplicateFlowActionStep = refetchedDuplicateFlowSteps[1]; - expect(response.body).toStrictEqual(expectedPayload); - expect(refetchedDuplicateFlow.userId).toStrictEqual(currentUser.id); - - expect(refetchedDuplicateFlowSteps.length).toStrictEqual(2); - - expect(refetchedDuplicateFlowTriggerStep.appKey).toStrictEqual( - triggerStep.appKey - ); - - expect(refetchedDuplicateFlowTriggerStep.key).toStrictEqual( - triggerStep.key - ); - - expect(refetchedDuplicateFlowTriggerStep.connectionId).toStrictEqual( - triggerStep.connectionId - ); - - expect(refetchedDuplicateFlowTriggerStep.position).toStrictEqual( - triggerStep.position - ); - - expect(refetchedDuplicateFlowTriggerStep.parameters).toStrictEqual( - triggerStep.parameters - ); - - expect(refetchedDuplicateFlowTriggerStep.type).toStrictEqual( - triggerStep.type - ); - - expect(refetchedDuplicateFlowActionStep.appKey).toStrictEqual( - actionStep.appKey - ); - - expect(refetchedDuplicateFlowActionStep.key).toStrictEqual(actionStep.key); - - expect(refetchedDuplicateFlowActionStep.connectionId).toStrictEqual( - actionStep.connectionId - ); - - expect(refetchedDuplicateFlowActionStep.position).toStrictEqual( - actionStep.position - ); - - expect(refetchedDuplicateFlowActionStep.parameters.topic).toStrictEqual( - actionStep.parameters.topic - ); - - expect(refetchedDuplicateFlowActionStep.parameters.message).toStrictEqual( - actionStep.parameters.message.replaceAll( - `{{step.${triggerStep.id}.`, - `{{step.${refetchedDuplicateFlowTriggerStep.id}.` - ) - ); - - expect(refetchedDuplicateFlowActionStep.type).toStrictEqual( - actionStep.type - ); - - expect(refetchedDuplicateFlowTriggerStep.webhookPath).toStrictEqual( - `/webhooks/flows/${refetchedDuplicateFlow.id}` - ); }); it('should return not found response for not existing flow UUID', async () => {