From 7664b585536c57e7323fdb5e6ced45ad40748c14 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Fri, 22 Mar 2024 02:55:23 +0100 Subject: [PATCH] feat: Implement create dynamic fields API endpoint --- .../api/v1/steps/create-dynamic-fields.js | 17 ++ .../v1/steps/create-dynamic-fields.test.js | 169 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/models/step.js | 21 +++ packages/backend/src/routes/api/v1/steps.js | 8 + .../api/v1/steps/create-dynamic-fields.js | 36 ++++ 6 files changed, 255 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js create mode 100644 packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js new file mode 100644 index 00000000..f1315dfa --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js @@ -0,0 +1,17 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .clone() + .where('steps.id', request.params.stepId) + .whereNotNull('steps.app_key') + .first() + .throwIfNotFound(); + + const dynamicFields = await step.createDynamicFields( + request.body.dynamicFieldsKey, + request.body.parameters + ); + + renderObject(response, dynamicFields); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js new file mode 100644 index 00000000..75e27dcb --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js @@ -0,0 +1,169 @@ +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 { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields'; + +describe('POST /api/v1/steps/:stepId/dynamic-fields', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return dynamically created fields of the current users step', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const actionStep = await createStep({ + flowId: currentUserflow.id, + type: 'action', + appKey: 'slack', + key: 'sendMessageToChannel', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-fields`) + .set('Authorization', token) + .send({ + dynamicFieldsKey: 'listFieldsAfterSendAsBot', + parameters: { + sendAsBot: true, + }, + }) + .expect(200); + + const expectedPayload = await createDynamicFieldsMock(); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return dynamically created fields of the another users step', async () => { + const anotherUser = await createUser(); + const anotherUserflow = await createFlow({ userId: anotherUser.id }); + + const actionStep = await createStep({ + flowId: anotherUserflow.id, + type: 'action', + appKey: 'slack', + key: 'sendMessageToChannel', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-fields`) + .set('Authorization', token) + .send({ + dynamicFieldsKey: 'listFieldsAfterSendAsBot', + parameters: { + sendAsBot: true, + }, + }) + .expect(200); + + const expectedPayload = await createDynamicFieldsMock(); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingStepUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for existing step UUID without app key', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const step = await createStep({ appKey: null }); + + await request(app) + .get(`/api/v1/steps/${step.id}/dynamic-fields`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .post('/api/v1/steps/invalidStepUUID/dynamic-fields') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index e1cd1771..c3abf6a8 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -23,6 +23,10 @@ const authorizationList = { action: 'update', subject: 'Flow', }, + 'POST /api/v1/steps/:stepId/dynamic-fields': { + action: 'update', + subject: 'Flow', + }, 'GET /api/v1/connections/:connectionId/flows': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index 1000b263..9304afa5 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -7,6 +7,7 @@ import Connection from './connection.js'; import ExecutionStep from './execution-step.js'; import Telemetry from '../helpers/telemetry/index.js'; import appConfig from '../config/app.js'; +import globalVariable from '../helpers/global-variable.js'; class Step extends Base { static tableName = 'steps'; @@ -196,6 +197,26 @@ class Step extends Base { return existingArguments; } + async createDynamicFields(dynamicFieldsKey, parameters) { + const connection = await this.$relatedQuery('connection'); + const flow = await this.$relatedQuery('flow'); + const app = await this.getApp(); + const $ = await globalVariable({ connection, app, flow, step: this }); + + const command = app.dynamicFields.find( + (data) => data.key === dynamicFieldsKey + ); + + for (const parameterKey in parameters) { + const parameterValue = parameters[parameterKey]; + $.step.parameters[parameterKey] = parameterValue; + } + + const dynamicFields = (await command.run($)) || []; + + return dynamicFields; + } + async updateWebhookUrl() { if (this.isAction) return this; diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js index 36dad046..674b9da0 100644 --- a/packages/backend/src/routes/api/v1/steps.js +++ b/packages/backend/src/routes/api/v1/steps.js @@ -4,6 +4,7 @@ import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; +import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js'; const router = Router(); @@ -21,4 +22,11 @@ router.get( asyncHandler(getPreviousStepsAction) ); +router.post( + '/:stepId/dynamic-fields', + authenticateUser, + authorizeUser, + asyncHandler(createDynamicFieldsAction) +); + export default router; diff --git a/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js b/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js new file mode 100644 index 00000000..e7b16057 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js @@ -0,0 +1,36 @@ +const createDynamicFieldsMock = async () => { + const data = [ + { + label: 'Bot name', + key: 'botName', + type: 'string', + required: true, + value: 'Automatisch', + description: + 'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.', + variables: true, + }, + { + label: 'Bot icon', + key: 'botIcon', + type: 'string', + required: false, + description: + 'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:', + variables: true, + }, + ]; + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default createDynamicFieldsMock;