diff --git a/packages/backend/src/controllers/api/v1/flows/create-flow.js b/packages/backend/src/controllers/api/v1/flows/create-flow.js index 39d12f33..36c46ac5 100644 --- a/packages/backend/src/controllers/api/v1/flows/create-flow.js +++ b/packages/backend/src/controllers/api/v1/flows/create-flow.js @@ -1,11 +1,11 @@ import { renderObject } from '../../../../helpers/renderer.js'; export default async (request, response) => { - const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({ - name: 'Name your flow', - }); + const { templateId } = request.query; - await flow.createInitialSteps(); + const flow = templateId + ? await request.currentUser.createFlowFromTemplate(templateId) + : await request.currentUser.createEmptyFlow(); renderObject(response, flow, { status: 201 }); }; diff --git a/packages/backend/src/controllers/api/v1/flows/create-flow.test.js b/packages/backend/src/controllers/api/v1/flows/create-flow.test.js index fb8f5635..2c55f8ef 100644 --- a/packages/backend/src/controllers/api/v1/flows/create-flow.test.js +++ b/packages/backend/src/controllers/api/v1/flows/create-flow.test.js @@ -4,6 +4,7 @@ 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 { createTemplate } from '../../../../../test/factories/template.js'; import createFlowMock from '../../../../../test/mocks/rest/api/v1/flows/create-flow.js'; import { createPermission } from '../../../../../test/factories/permission.js'; @@ -17,7 +18,7 @@ describe('POST /api/v1/flows', () => { token = await createAuthTokenByUserId(currentUser.id); }); - it('should return created flow', async () => { + it('should create an empty flow when no templateId is provided', async () => { await createPermission({ action: 'create', subject: 'Flow', @@ -38,4 +39,25 @@ describe('POST /api/v1/flows', () => { expect(response.body).toMatchObject(expectedPayload); }); + + it('should create a flow from template when templateId is provided', async () => { + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const template = await createTemplate({ + name: 'Sample template', + }); + + const response = await request(app) + .post('/api/v1/flows') + .query({ templateId: template.id }) + .set('Authorization', token) + .expect(201); + + expect(response.body.data.name).toBe(template.flowData.name); + }); }); diff --git a/packages/backend/src/controllers/api/v1/flows/import-flow.js b/packages/backend/src/controllers/api/v1/flows/import-flow.js index c64d0f9e..5b6a2383 100644 --- a/packages/backend/src/controllers/api/v1/flows/import-flow.js +++ b/packages/backend/src/controllers/api/v1/flows/import-flow.js @@ -1,12 +1,8 @@ import { renderObject } from '../../../../helpers/renderer.js'; -import importFlow from '../../../../helpers/import-flow.js'; +import Flow from '../../../../models/flow.js'; export default async function importFlowController(request, response) { - const flow = await importFlow( - request.currentUser, - flowParams(request), - response - ); + const flow = await Flow.import(request.currentUser, flowParams(request)); return renderObject(response, flow, { status: 201 }); } diff --git a/packages/backend/src/helpers/import-flow.js b/packages/backend/src/helpers/import-flow.js index ae89559a..da8faa6e 100644 --- a/packages/backend/src/helpers/import-flow.js +++ b/packages/backend/src/helpers/import-flow.js @@ -1,14 +1,12 @@ import Crypto from 'crypto'; import Step from '../models/step.js'; -import { renderObjectionError } from './renderer.js'; +import { ValidationError } from 'objection'; -const importFlow = async (user, flowData, response) => { +const importFlow = async (user, flowData) => { const steps = flowData.steps || []; - // Validation: the first step must be a trigger if (!steps.length || steps[0].type !== 'trigger') { - return renderObjectionError(response, { - statusCode: 422, + throw new ValidationError({ type: 'ValidationError', data: { steps: [{ message: 'The first step must be a trigger!' }], diff --git a/packages/backend/src/models/flow.js b/packages/backend/src/models/flow.js index bd4a8058..9c05868b 100644 --- a/packages/backend/src/models/flow.js +++ b/packages/backend/src/models/flow.js @@ -9,6 +9,7 @@ import globalVariable from '../helpers/global-variable.js'; import logger from '../helpers/logger.js'; import Telemetry from '../helpers/telemetry/index.js'; import exportFlow from '../helpers/export-flow.js'; +import importFlow from '../helpers/import-flow.js'; import flowQueue from '../queues/flow.js'; import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, @@ -99,6 +100,10 @@ class Flow extends Base { }, }); + static async import(user, flowData) { + return importFlow(user, flowData); + } + static async populateStatusProperty(flows) { const referenceFlow = flows[0]; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index cb1e2156..27bbe8b2 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -22,6 +22,7 @@ import Step from './step.js'; import Subscription from './subscription.ee.js'; import Folder from './folder.js'; import UsageData from './usage-data.ee.js'; +import Template from './template.ee.js'; import Billing from '../helpers/billing/index.ee.js'; import NotAuthorizedError from '../errors/not-authorized.js'; @@ -675,6 +676,26 @@ class User extends Base { } } + async createEmptyFlow() { + const flow = await this.$relatedQuery('flows').insertAndFetch({ + name: 'Name your flow', + }); + + await flow.createInitialSteps(); + + return flow; + } + + async createFlowFromTemplate(templateId) { + const template = await Template.query() + .findById(templateId) + .throwIfNotFound(); + + const flow = await Flow.import(this, template.flowData); + + return flow; + } + async $beforeInsert(queryContext) { await super.$beforeInsert(queryContext); diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index 4eae81c0..7a12abb7 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -34,7 +34,9 @@ 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 { createTemplate } from '../../test/factories/template.js'; import Billing from '../helpers/billing/index.ee.js'; +import Template from './template.ee.js'; describe('User model', () => { it('tableName should return correct name', () => { @@ -1507,6 +1509,61 @@ describe('User model', () => { }); }); + describe('createEmptyFlow', () => { + it('should create a flow with default name', async () => { + const user = await createUser(); + const flow = await user.createEmptyFlow(); + + expect(flow.name).toBe('Name your flow'); + expect(flow.userId).toBe(user.id); + }); + + it('should call createInitialSteps on the created flow', async () => { + const user = await createUser(); + const createInitialStepsSpy = vi.spyOn( + Flow.prototype, + 'createInitialSteps' + ); + + await user.createEmptyFlow(); + + expect(createInitialStepsSpy).toHaveBeenCalledOnce(); + }); + }); + + describe('createFlowFromTemplate', () => { + let user, template; + + beforeEach(async () => { + user = await createUser(); + template = await createTemplate(); + }); + + it('should throw an error if template is not found', async () => { + const nonExistentTemplateId = Crypto.randomUUID(); + + await expect( + user.createFlowFromTemplate(nonExistentTemplateId) + ).rejects.toThrow('NotFoundError'); + }); + + it('should call Flow.import with the correct parameters', async () => { + vi.spyOn(Template.query(), 'findById').mockImplementation(() => ({ + throwIfNotFound: () => template, + })); + + const importSpy = vi.spyOn(Flow, 'import').mockResolvedValue({ + id: Crypto.randomUUID(), + name: template.flowData.name, + steps: [], + }); + + await user.createFlowFromTemplate(template.id); + + expect(importSpy).toHaveBeenCalledWith(user, template.flowData); + }); + }); + describe('$beforeInsert', () => { it('should call super.$beforeInsert', async () => { const superBeforeInsertSpy = vi