From bd0732347dad5d593de667c643ebfaab2d92b9eb Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 24 Apr 2025 23:03:42 +0000 Subject: [PATCH] feat(api): add create flow endpoint for users --- .../api/v1/flows/create-flow.ee.test.js | 74 +++++++++++++++++++ .../api/v1/users/create-flow.ee.js | 16 ++++ .../backend/src/routes/api/v1/users.ee.js | 2 + .../mocks/rest/api/v1/users/create-flow.js | 23 ++++++ packages/backend/vitest.config.js | 4 +- 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/controllers/api/v1/flows/create-flow.ee.test.js create mode 100644 packages/backend/src/controllers/api/v1/users/create-flow.ee.js create mode 100644 packages/backend/test/mocks/rest/api/v1/users/create-flow.js diff --git a/packages/backend/src/controllers/api/v1/flows/create-flow.ee.test.js b/packages/backend/src/controllers/api/v1/flows/create-flow.ee.test.js new file mode 100644 index 00000000..2d0b8b34 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/create-flow.ee.test.js @@ -0,0 +1,74 @@ +import Crypto from 'node:crypto'; +import request from 'supertest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import app from '../../../../app.js'; +import { createApiToken } from '../../../../../test/factories/api-token.js'; +import { createTemplate } from '../../../../../test/factories/template.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import createFlowMock from '../../../../../test/mocks/rest/api/v1/users/create-flow.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('POST /api/v1/users/:userId/flows', () => { + let user, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + user = await createUser(); + token = (await createApiToken()).token; + }); + + it('should create an empty flow when no templateId is provided for the given user', async () => { + const response = await request(app) + .post(`/api/v1/users/${user.id}/flows`) + .set('x-api-token', token) + .expect(201); + + const refetchedFlow = await user + .$relatedQuery('flows') + .findById(response.body.data.id); + + const expectedPayload = await createFlowMock(refetchedFlow); + + expect(response.body).toMatchObject(expectedPayload); + }); + + it('should create a flow from template when templateId is provided for the given user', async () => { + const template = await createTemplate({ + name: 'Sample template', + }); + + const response = await request(app) + .post(`/api/v1/users/${user.id}/flows`) + .query({ templateId: template.id }) + .set('x-api-token', token) + .expect(201); + + expect(response.body.data.name).toBe(template.flowData.name); + }); + + it('should return an error when an invalid templateId is provided', async () => { + await request(app) + .post(`/api/v1/users/${user.id}/flows`) + .query({ templateId: 'invalid-template-id' }) + .set('x-api-token', token) + .expect(400); + }); + + it('should respond with HTTP 404 for non-existent user', async () => { + const notExistingUserId = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/users/${notExistingUserId}/folders`) + .set('x-api-token', token) + .expect(404); + }); + + it('should return bad request response for invalid user UUID', async () => { + await request(app) + .get(`/api/v1/users/invalidUserUUID/folders`) + .set('x-api-token', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/create-flow.ee.js b/packages/backend/src/controllers/api/v1/users/create-flow.ee.js new file mode 100644 index 00000000..184037d9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/create-flow.ee.js @@ -0,0 +1,16 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import User from '../../../../models/user.js'; + +export default async (request, response) => { + const { templateId } = request.query; + + const user = await User.query() + .findById(request.params.userId) + .throwIfNotFound(); + + const flow = templateId + ? await user.createFlowFromTemplate(templateId) + : await user.createEmptyFlow(); + + renderObject(response, flow, { status: 201 }); +}; diff --git a/packages/backend/src/routes/api/v1/users.ee.js b/packages/backend/src/routes/api/v1/users.ee.js index b8ac2fed..0bf2fd7e 100644 --- a/packages/backend/src/routes/api/v1/users.ee.js +++ b/packages/backend/src/routes/api/v1/users.ee.js @@ -1,9 +1,11 @@ import { Router } from 'express'; import getFoldersAction from '../../../controllers/api/v1/users/get-folders.ee.js'; import createFolderAction from '../../../controllers/api/v1/users/create-folder.ee.js'; +import createFlowAction from '../../../controllers/api/v1/users/create-flow.ee.js'; const router = Router(); +router.post('/:userId/flows', createFlowAction); router.get('/:userId/folders', getFoldersAction); router.post('/:userId/folders', createFolderAction); diff --git a/packages/backend/test/mocks/rest/api/v1/users/create-flow.js b/packages/backend/test/mocks/rest/api/v1/users/create-flow.js new file mode 100644 index 00000000..cb7606cb --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/create-flow.js @@ -0,0 +1,23 @@ +const createFlowMock = async (flow) => { + const data = { + id: flow.id, + active: flow.active, + name: flow.name, + status: flow.status, + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Flow', + }, + }; +}; + +export default createFlowMock; diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index a7b0d9a9..11379059 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -29,8 +29,8 @@ export default defineConfig({ thresholds: { autoUpdate: true, statements: 99.4, - branches: 98.32, - functions: 99.05, + branches: 98.33, + functions: 99.06, lines: 99.4, }, },