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',
|
action: 'update',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
},
|
},
|
||||||
|
'POST /api/v1/steps/:stepId/dynamic-data': {
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
},
|
||||||
'GET /api/v1/connections/:connectionId/flows': {
|
'GET /api/v1/connections/:connectionId/flows': {
|
||||||
action: 'read',
|
action: 'read',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
|
@@ -2,6 +2,7 @@ import logger from './logger.js';
|
|||||||
import objection from 'objection';
|
import objection from 'objection';
|
||||||
import * as Sentry from './sentry.ee.js';
|
import * as Sentry from './sentry.ee.js';
|
||||||
const { NotFoundError, DataError } = objection;
|
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
|
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
@@ -18,6 +19,17 @@ const errorHandler = (error, request, response, next) => {
|
|||||||
response.status(400).end();
|
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;
|
const statusCode = error.statusCode || 500;
|
||||||
|
|
||||||
logger.error(request.method + ' ' + request.url + ' ' + statusCode);
|
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 Telemetry from '../helpers/telemetry/index.js';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import globalVariable from '../helpers/global-variable.js';
|
import globalVariable from '../helpers/global-variable.js';
|
||||||
|
import computeParameters from '../helpers/compute-parameters.js';
|
||||||
|
|
||||||
class Step extends Base {
|
class Step extends Base {
|
||||||
static tableName = 'steps';
|
static tableName = 'steps';
|
||||||
@@ -217,6 +218,39 @@ class Step extends Base {
|
|||||||
return dynamicFields;
|
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() {
|
async updateWebhookUrl() {
|
||||||
if (this.isAction) return this;
|
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 getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js';
|
||||||
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
|
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
|
||||||
import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.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();
|
const router = Router();
|
||||||
|
|
||||||
@@ -29,4 +30,11 @@ router.post(
|
|||||||
asyncHandler(createDynamicFieldsAction)
|
asyncHandler(createDynamicFieldsAction)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:stepId/dynamic-data',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
asyncHandler(createDynamicDataAction)
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@@ -19,6 +19,8 @@ export const createStep = async (params = {}) => {
|
|||||||
params.appKey =
|
params.appKey =
|
||||||
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
|
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
|
||||||
|
|
||||||
|
params.parameters = params?.parameters || {};
|
||||||
|
|
||||||
const step = await Step.query().insertAndFetch(params);
|
const step = await Step.query().insertAndFetch(params);
|
||||||
|
|
||||||
return step;
|
return step;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import { client as knex } from '../../src/config/database.js';
|
import { client as knex } from '../../src/config/database.js';
|
||||||
import logger from '../../src/helpers/logger.js';
|
import logger from '../../src/helpers/logger.js';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
global.beforeAll(async () => {
|
global.beforeAll(async () => {
|
||||||
global.knex = null;
|
global.knex = null;
|
||||||
@@ -22,8 +23,8 @@ global.afterEach(async () => {
|
|||||||
await global.knex.rollback();
|
await global.knex.rollback();
|
||||||
Model.knex(knex);
|
Model.knex(knex);
|
||||||
|
|
||||||
// jest.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
// jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
global.afterAll(async () => {
|
global.afterAll(async () => {
|
||||||
|
Reference in New Issue
Block a user