From 8f7f6dc19e27b63448d15449ae7d50606b6d8546 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 13 Mar 2024 13:11:54 +0100 Subject: [PATCH 1/3] feat: Add connection serializer --- .../backend/src/serializers/connection.js | 16 +++++++++++ .../src/serializers/connection.test.js | 28 +++++++++++++++++++ packages/backend/src/serializers/index.js | 2 ++ 3 files changed, 46 insertions(+) create mode 100644 packages/backend/src/serializers/connection.js create mode 100644 packages/backend/src/serializers/connection.test.js diff --git a/packages/backend/src/serializers/connection.js b/packages/backend/src/serializers/connection.js new file mode 100644 index 00000000..e285f1e2 --- /dev/null +++ b/packages/backend/src/serializers/connection.js @@ -0,0 +1,16 @@ +const connectionSerializer = (connection) => { + return { + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + verified: connection.verified, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; +}; + +export default connectionSerializer; diff --git a/packages/backend/src/serializers/connection.test.js b/packages/backend/src/serializers/connection.test.js new file mode 100644 index 00000000..2a4df3c3 --- /dev/null +++ b/packages/backend/src/serializers/connection.test.js @@ -0,0 +1,28 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createConnection } from '../../test/factories/connection'; +import connectionSerializer from './connection'; + +describe('connectionSerializer', () => { + let connection; + + beforeEach(async () => { + connection = await createConnection(); + }); + + it('should return connection data', async () => { + const expectedPayload = { + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + verified: connection.verified, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + expect(connectionSerializer(connection)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 795dd53b..e02e203f 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -6,6 +6,7 @@ import appAuthClientSerializer from './app-auth-client.js'; import appConfigSerializer from './app-config.js'; import flowSerializer from './flow.js'; import stepSerializer from './step.js'; +import connectionSerializer from './connection.js'; import appSerializer from './app.js'; import authSerializer from './auth.js'; import triggerSerializer from './trigger.js'; @@ -23,6 +24,7 @@ const serializers = { AppConfig: appConfigSerializer, Flow: flowSerializer, Step: stepSerializer, + Connection: connectionSerializer, App: appSerializer, Auth: authSerializer, Trigger: triggerSerializer, From d6923a2ff0a9d4689a790c1fe86f358e2a652b8a Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 13 Mar 2024 13:13:15 +0100 Subject: [PATCH 2/3] fix: Use insertAndFetch to get record with after find modifications --- packages/backend/test/factories/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/factories/connection.js b/packages/backend/test/factories/connection.js index b7188ee0..9692a3ab 100644 --- a/packages/backend/test/factories/connection.js +++ b/packages/backend/test/factories/connection.js @@ -17,7 +17,7 @@ export const createConnection = async (params = {}) => { appConfig.encryptionKey ).toString(); - const connection = await Connection.query().insert(params).returning('*'); + const connection = await Connection.query().insertAndFetch(params); return connection; }; From 9a0434be3268b813a2c506ca08feb72ded47e991 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 13 Mar 2024 15:24:50 +0100 Subject: [PATCH 3/3] feat: Implement get step connection API endpoint --- .../api/v1/steps/get-connection.js | 11 ++ .../api/v1/steps/get-connection.test.js | 121 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/models/user.js | 5 + packages/backend/src/routes/api/v1/steps.js | 16 +++ packages/backend/src/routes/index.js | 2 + .../mocks/rest/api/v1/steps/get-connection.js | 27 ++++ 7 files changed, 186 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/steps/get-connection.js create mode 100644 packages/backend/src/controllers/api/v1/steps/get-connection.test.js create mode 100644 packages/backend/src/routes/api/v1/steps.js create mode 100644 packages/backend/test/mocks/rest/api/v1/steps/get-connection.js diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.js b/packages/backend/src/controllers/api/v1/steps/get-connection.js new file mode 100644 index 00000000..ab1a403e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .findById(request.params.stepId) + .throwIfNotFound(); + + const connection = await step.$relatedQuery('connection').throwIfNotFound(); + + renderObject(response, connection); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.test.js b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js new file mode 100644 index 00000000..003b80bd --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js @@ -0,0 +1,121 @@ +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'; +import { createUser } from '../../../../../test/factories/user'; +import { createConnection } from '../../../../../test/factories/connection'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getConnectionMock from '../../../../../test/mocks/rest/api/v1/steps/get-connection'; + +describe('GET /api/v1/steps/:stepId/connection', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the current user connection data of specified step', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const currentUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: currentUserflow.id, + connectionId: currentUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(currentUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the current user connection data of specified step', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const anotherUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(anotherUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step without connection', async () => { + const stepWithoutConnection = await createStep(); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get(`/api/v1/steps/${stepWithoutConnection.id}/connection`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingFlowUUID}/connection`) + .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: [], + }); + + await request(app) + .get('/api/v1/steps/invalidFlowUUID/connection') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index f294cba8..1822c1e0 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -15,6 +15,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/steps/:stepId/connection': { + action: 'read', + subject: 'Flow', + }, 'GET /api/v1/connections/:connectionId/flows': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 2d199881..d3dcaffe 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -149,6 +149,11 @@ class User extends Base { return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query(); } + get authorizedSteps() { + const conditions = this.can('read', 'Flow'); + return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); + } + get authorizedExecutions() { const conditions = this.can('read', 'Execution'); return conditions.isCreator diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js new file mode 100644 index 00000000..80fc2f0b --- /dev/null +++ b/packages/backend/src/routes/api/v1/steps.js @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; + +const router = Router(); + +router.get( + '/:stepId/connection', + authenticateUser, + authorizeUser, + asyncHandler(getConnectionAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index a51700ac..a542f7fc 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -9,6 +9,7 @@ import paymentRouter from './api/v1/payment.ee.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js'; import appConfigsRouter from './api/v1/app-configs.ee.js'; import flowsRouter from './api/v1/flows.js'; +import stepsRouter from './api/v1/steps.js'; import appsRouter from './api/v1/apps.js'; import connectionsRouter from './api/v1/connections.js'; import executionsRouter from './api/v1/executions.js'; @@ -30,6 +31,7 @@ router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter); router.use('/api/v1/app-configs', appConfigsRouter); router.use('/api/v1/flows', flowsRouter); +router.use('/api/v1/steps', stepsRouter); router.use('/api/v1/apps', appsRouter); router.use('/api/v1/connections', connectionsRouter); router.use('/api/v1/executions', executionsRouter); diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js new file mode 100644 index 00000000..3f6c8abb --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js @@ -0,0 +1,27 @@ +const getConnectionMock = async (connection) => { + const data = { + id: connection.id, + key: connection.key, + verified: connection.verified, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default getConnectionMock;