feat: Implement create dynamic data API endpoint
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
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')
|
||||
.whereNotNull('steps.connection_id')
|
||||
.first()
|
||||
.throwIfNotFound();
|
||||
|
||||
const dynamicData = await step.createDynamicData(
|
||||
request.body.dynamicDataKey,
|
||||
request.body.parameters
|
||||
);
|
||||
|
||||
renderObject(response, dynamicData);
|
||||
};
|
@@ -0,0 +1,244 @@
|
||||
import { vi, 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 listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js';
|
||||
import HttpError from '../../../../errors/http.js';
|
||||
|
||||
describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
||||
let currentUser, currentUserRole, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
currentUserRole = await currentUser.$relatedQuery('role');
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
describe('should return dynamically created data', () => {
|
||||
let repositories;
|
||||
|
||||
beforeEach(async () => {
|
||||
repositories = [
|
||||
{
|
||||
value: 'automatisch/automatisch',
|
||||
name: 'automatisch/automatisch',
|
||||
},
|
||||
{
|
||||
value: 'automatisch/sample',
|
||||
name: 'automatisch/sample',
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
|
||||
return {
|
||||
data: repositories,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('of the current users step', async () => {
|
||||
const currentUserFlow = await createFlow({ userId: currentUser.id });
|
||||
const connection = await createConnection({ userId: currentUser.id });
|
||||
|
||||
const actionStep = await createStep({
|
||||
flowId: currentUserFlow.id,
|
||||
connectionId: connection.id,
|
||||
type: 'action',
|
||||
appKey: 'github',
|
||||
key: 'createIssue',
|
||||
});
|
||||
|
||||
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-data`)
|
||||
.set('Authorization', token)
|
||||
.send({
|
||||
dynamicDataKey: 'listRepos',
|
||||
parameters: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toEqual(repositories);
|
||||
});
|
||||
|
||||
it('of the another users step', async () => {
|
||||
const anotherUser = await createUser();
|
||||
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||
const connection = await createConnection({ userId: anotherUser.id });
|
||||
|
||||
const actionStep = await createStep({
|
||||
flowId: anotherUserFlow.id,
|
||||
connectionId: connection.id,
|
||||
type: 'action',
|
||||
appKey: 'github',
|
||||
key: 'createIssue',
|
||||
});
|
||||
|
||||
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-data`)
|
||||
.set('Authorization', token)
|
||||
.send({
|
||||
dynamicDataKey: 'listRepos',
|
||||
parameters: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data).toEqual(repositories);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return error for dynamically created data', () => {
|
||||
let errors;
|
||||
|
||||
beforeEach(async () => {
|
||||
errors = {
|
||||
message: 'Not Found',
|
||||
documentation_url:
|
||||
'https://docs.github.com/rest/users/users#get-a-user',
|
||||
};
|
||||
|
||||
vi.spyOn(listRepos, 'run').mockImplementation(async () => {
|
||||
throw new HttpError({ message: errors });
|
||||
});
|
||||
});
|
||||
|
||||
it('of the current users step', async () => {
|
||||
const currentUserFlow = await createFlow({ userId: currentUser.id });
|
||||
const connection = await createConnection({ userId: currentUser.id });
|
||||
|
||||
const actionStep = await createStep({
|
||||
flowId: currentUserFlow.id,
|
||||
connectionId: connection.id,
|
||||
type: 'action',
|
||||
appKey: 'github',
|
||||
key: 'createIssue',
|
||||
});
|
||||
|
||||
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-data`)
|
||||
.set('Authorization', token)
|
||||
.send({
|
||||
dynamicDataKey: 'listRepos',
|
||||
parameters: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.errors).toEqual(errors);
|
||||
});
|
||||
});
|
||||
|
||||
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-data`)
|
||||
.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-data`)
|
||||
.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);
|
||||
});
|
||||
});
|
@@ -27,6 +27,10 @@ const authorizationList = {
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
},
|
||||
'POST /api/v1/steps/:stepId/dynamic-data': {
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
},
|
||||
'GET /api/v1/connections/:connectionId/flows': {
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
|
@@ -2,6 +2,7 @@ import logger from './logger.js';
|
||||
import objection from 'objection';
|
||||
import * as Sentry from './sentry.ee.js';
|
||||
const { NotFoundError, DataError } = objection;
|
||||
import HttpError from '../errors/http.js';
|
||||
|
||||
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -18,6 +19,17 @@ const errorHandler = (error, request, response, next) => {
|
||||
response.status(400).end();
|
||||
}
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const httpErrorPayload = {
|
||||
errors: JSON.parse(error.message),
|
||||
meta: {
|
||||
type: 'HttpError',
|
||||
},
|
||||
};
|
||||
|
||||
response.status(200).json(httpErrorPayload);
|
||||
}
|
||||
|
||||
const statusCode = error.statusCode || 500;
|
||||
|
||||
logger.error(request.method + ' ' + request.url + ' ' + statusCode);
|
||||
|
@@ -8,6 +8,7 @@ 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';
|
||||
import computeParameters from '../helpers/compute-parameters.js';
|
||||
|
||||
class Step extends Base {
|
||||
static tableName = 'steps';
|
||||
@@ -217,6 +218,39 @@ class Step extends Base {
|
||||
return dynamicFields;
|
||||
}
|
||||
|
||||
async createDynamicData(dynamicDataKey, 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.dynamicData.find((data) => data.key === dynamicDataKey);
|
||||
|
||||
for (const parameterKey in parameters) {
|
||||
const parameterValue = parameters[parameterKey];
|
||||
$.step.parameters[parameterKey] = parameterValue;
|
||||
}
|
||||
|
||||
const lastExecution = await flow.$relatedQuery('lastExecution');
|
||||
const lastExecutionId = lastExecution?.id;
|
||||
|
||||
const priorExecutionSteps = lastExecutionId
|
||||
? await ExecutionStep.query().where({
|
||||
execution_id: lastExecutionId,
|
||||
})
|
||||
: [];
|
||||
|
||||
const computedParameters = computeParameters(
|
||||
$.step.parameters,
|
||||
priorExecutionSteps
|
||||
);
|
||||
|
||||
$.step.parameters = computedParameters;
|
||||
const dynamicData = (await command.run($)).data;
|
||||
|
||||
return dynamicData;
|
||||
}
|
||||
|
||||
async updateWebhookUrl() {
|
||||
if (this.isAction) return this;
|
||||
|
||||
|
@@ -5,6 +5,7 @@ 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';
|
||||
import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -29,4 +30,11 @@ router.post(
|
||||
asyncHandler(createDynamicFieldsAction)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:stepId/dynamic-data',
|
||||
authenticateUser,
|
||||
authorizeUser,
|
||||
asyncHandler(createDynamicDataAction)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@@ -19,6 +19,8 @@ export const createStep = async (params = {}) => {
|
||||
params.appKey =
|
||||
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
|
||||
|
||||
params.parameters = params?.parameters || {};
|
||||
|
||||
const step = await Step.query().insertAndFetch(params);
|
||||
|
||||
return step;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Model } from 'objection';
|
||||
import { client as knex } from '../../src/config/database.js';
|
||||
import logger from '../../src/helpers/logger.js';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
global.beforeAll(async () => {
|
||||
global.knex = null;
|
||||
@@ -22,8 +23,8 @@ global.afterEach(async () => {
|
||||
await global.knex.rollback();
|
||||
Model.knex(knex);
|
||||
|
||||
// jest.restoreAllMocks();
|
||||
// jest.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
global.afterAll(async () => {
|
||||
|
Reference in New Issue
Block a user