Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement get previous steps rest API endpoint #1755

Merged
merged 2 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ref } from 'objection';
import ExecutionStep from '../../../../models/execution-step.js';
import { renderObject } from '../../../../helpers/renderer.js';

export default async (request, response) => {
const step = await request.currentUser.authorizedSteps
.clone()
.findOne({ 'steps.id': request.params.stepId })
.throwIfNotFound();

const previousSteps = await request.currentUser.authorizedSteps
.clone()
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)
.andWhere(
'executionSteps.created_at',
'=',
ExecutionStep.query()
.max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');

renderObject(response, previousSteps);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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 { createExecutionStep } from '../../../../../test/factories/execution-step.js';
import { createPermission } from '../../../../../test/factories/permission';
import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps';

describe('GET /api/v1/steps/:stepId/previous-steps', () => {
let currentUser, currentUserRole, token;

beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');

token = createAuthTokenByUserId(currentUser.id);
});

it('should return the previous steps of the specified step of the current user', async () => {
const currentUserflow = await createFlow({ userId: currentUser.id });

const triggerStep = await createStep({
flowId: currentUserflow.id,
type: 'trigger',
});

const actionStepOne = await createStep({
flowId: currentUserflow.id,
type: 'action',
});

const actionStepTwo = await createStep({
flowId: currentUserflow.id,
type: 'action',
});

const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});

const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});

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)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);

const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);

expect(response.body).toEqual(expectedPayload);
});

it('should return the previous steps of the specified step of another user', async () => {
const anotherUser = await createUser();
const anotherUserFlow = await createFlow({ userId: anotherUser.id });

const triggerStep = await createStep({
flowId: anotherUserFlow.id,
type: 'trigger',
});

const actionStepOne = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});

const actionStepTwo = await createStep({
flowId: anotherUserFlow.id,
type: 'action',
});

const executionStepOne = await createExecutionStep({
stepId: triggerStep.id,
});

const executionStepTwo = await createExecutionStep({
stepId: actionStepOne.id,
});

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)
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
.set('Authorization', token)
.expect(200);

const expectedPayload = await getPreviousStepsMock(
[triggerStep, actionStepOne],
[executionStepOne, executionStepTwo]
);

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 notExistingFlowUUID = Crypto.randomUUID();

await request(app)
.get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`)
.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)
.get('/api/v1/steps/invalidFlowUUID/previous-steps')
.set('Authorization', token)
.expect(400);
});
});
4 changes: 4 additions & 0 deletions packages/backend/src/helpers/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const authorizationList = {
action: 'read',
subject: 'Flow',
},
'GET /api/v1/steps/:stepId/previous-steps': {
action: 'update',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': {
action: 'read',
subject: 'Flow',
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/routes/api/v1/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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';
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';

const router = Router();

Expand All @@ -13,4 +14,11 @@ router.get(
asyncHandler(getConnectionAction)
);

router.get(
'/:stepId/previous-steps',
authenticateUser,
authorizeUser,
asyncHandler(getPreviousStepsAction)
);

export default router;
12 changes: 11 additions & 1 deletion packages/backend/src/serializers/step.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import executionStepSerializer from './execution-step.js';

const stepSerializer = (step) => {
return {
let stepData = {
id: step.id,
type: step.type,
key: step.key,
Expand All @@ -10,6 +12,14 @@ const stepSerializer = (step) => {
position: step.position,
parameters: step.parameters,
};

if (step.executionSteps?.length > 0) {
stepData.executionSteps = step.executionSteps.map((executionStep) =>
executionStepSerializer(executionStep)
);
}

return stepData;
};

export default stepSerializer;
18 changes: 18 additions & 0 deletions packages/backend/src/serializers/step.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { createStep } from '../../test/factories/step';
import { createExecutionStep } from '../../test/factories/execution-step';
import stepSerializer from './step';
import executionStepSerializer from './execution-step';

describe('stepSerializer', () => {
let step;
Expand All @@ -24,4 +26,20 @@ describe('stepSerializer', () => {

expect(stepSerializer(step)).toEqual(expectedPayload);
});

it('should return step data with the execution steps', async () => {
const executionStepOne = await createExecutionStep({ stepId: step.id });
const executionStepTwo = await createExecutionStep({ stepId: step.id });

step.executionSteps = [executionStepOne, executionStepTwo];

const expectedPayload = {
executionSteps: [
executionStepSerializer(executionStepOne),
executionStepSerializer(executionStepTwo),
],
};

expect(stepSerializer(step)).toMatchObject(expectedPayload);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const getPreviousStepsMock = async (steps, executionSteps) => {
const data = steps.map((step) => {
const filteredExecutionSteps = executionSteps.filter(
(executionStep) => executionStep.stepId === step.id
);

return {
id: step.id,
type: step.type,
key: step.key,
appKey: step.appKey,
iconUrl: step.iconUrl,
webhookUrl: step.webhookUrl,
status: step.status,
position: step.position,
parameters: step.parameters,
executionSteps: filteredExecutionSteps.map((executionStep) => ({
id: executionStep.id,
dataIn: executionStep.dataIn,
dataOut: executionStep.dataOut,
errorDetails: executionStep.errorDetails,
status: executionStep.status,
createdAt: executionStep.createdAt.getTime(),
updatedAt: executionStep.updatedAt.getTime(),
})),
};
});

return {
data: data,
meta: {
count: data.length,
currentPage: null,
isArray: true,
totalPages: null,
type: 'Step',
},
};
};

export default getPreviousStepsMock;