Merge pull request #1654 from automatisch/get-flow
feat: Implement get flow API endpoint
This commit is contained in:
11
packages/backend/src/controllers/api/v1/flows/get-flow.js
Normal file
11
packages/backend/src/controllers/api/v1/flows/get-flow.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const flow = await request.currentUser.authorizedFlows
|
||||||
|
.withGraphJoined({ steps: true })
|
||||||
|
.orderBy('steps.position', 'asc')
|
||||||
|
.findOne({ 'flows.id': request.params.flowId })
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
renderObject(response, flow);
|
||||||
|
};
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
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 getFlowMock from '../../../../../test/mocks/rest/api/v1/flows/get-flow';
|
||||||
|
|
||||||
|
describe('GET /api/v1/flows/:flowId', () => {
|
||||||
|
let currentUser, currentUserRole, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
currentUserRole = await currentUser.$relatedQuery('role');
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the flow data of current user', async () => {
|
||||||
|
const currentUserflow = await createFlow({ userId: currentUser.id });
|
||||||
|
const triggerStep = await createStep({ flowId: currentUserflow.id });
|
||||||
|
const actionStep = await createStep({ flowId: currentUserflow.id });
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/flows/${currentUserflow.id}`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getFlowMock(currentUserflow, [
|
||||||
|
triggerStep,
|
||||||
|
actionStep,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the flow data of another user', async () => {
|
||||||
|
const anotherUser = await createUser();
|
||||||
|
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||||
|
const triggerStep = await createStep({ flowId: anotherUserFlow.id });
|
||||||
|
const actionStep = await createStep({ flowId: anotherUserFlow.id });
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/flows/${anotherUserFlow.id}`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getFlowMock(anotherUserFlow, [
|
||||||
|
triggerStep,
|
||||||
|
actionStep,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -7,6 +7,10 @@ const authorizationList = {
|
|||||||
action: 'read',
|
action: 'read',
|
||||||
subject: 'User',
|
subject: 'User',
|
||||||
},
|
},
|
||||||
|
'GET /api/v1/flows/:flowId': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authorizeUser = async (request, response, next) => {
|
export const authorizeUser = async (request, response, next) => {
|
||||||
|
@@ -144,6 +144,11 @@ class User extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get authorizedFlows() {
|
||||||
|
const conditions = this.can('read', 'Flow');
|
||||||
|
return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query();
|
||||||
|
}
|
||||||
|
|
||||||
login(password) {
|
login(password) {
|
||||||
return bcrypt.compare(password, this.password);
|
return bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
10
packages/backend/src/routes/api/v1/flows.js
Normal file
10
packages/backend/src/routes/api/v1/flows.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||||
|
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||||
|
import getFlowAction from '../../../controllers/api/v1/flows/get-flow.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/:flowId', authenticateUser, authorizeUser, getFlowAction);
|
||||||
|
|
||||||
|
export default router;
|
@@ -7,6 +7,7 @@ import automatischRouter from './api/v1/automatisch.js';
|
|||||||
import usersRouter from './api/v1/users.js';
|
import usersRouter from './api/v1/users.js';
|
||||||
import paymentRouter from './api/v1/payment.ee.js';
|
import paymentRouter from './api/v1/payment.ee.js';
|
||||||
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
|
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
|
||||||
|
import flowsRouter from './api/v1/flows.js';
|
||||||
import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
|
import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
|
||||||
import rolesRouter from './api/v1/admin/roles.ee.js';
|
import rolesRouter from './api/v1/admin/roles.ee.js';
|
||||||
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
||||||
@@ -23,6 +24,7 @@ router.use('/api/v1/automatisch', automatischRouter);
|
|||||||
router.use('/api/v1/users', usersRouter);
|
router.use('/api/v1/users', usersRouter);
|
||||||
router.use('/api/v1/payment', paymentRouter);
|
router.use('/api/v1/payment', paymentRouter);
|
||||||
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
|
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
|
||||||
|
router.use('/api/v1/flows', flowsRouter);
|
||||||
router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter);
|
router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter);
|
||||||
router.use('/api/v1/admin/roles', rolesRouter);
|
router.use('/api/v1/admin/roles', rolesRouter);
|
||||||
router.use('/api/v1/admin/permissions', permissionsRouter);
|
router.use('/api/v1/admin/permissions', permissionsRouter);
|
||||||
|
18
packages/backend/src/serializers/flow.js
Normal file
18
packages/backend/src/serializers/flow.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import stepSerializer from './step.js';
|
||||||
|
|
||||||
|
const flowSerializer = (flow) => {
|
||||||
|
let flowData = {
|
||||||
|
id: flow.id,
|
||||||
|
name: flow.name,
|
||||||
|
active: flow.active,
|
||||||
|
status: flow.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (flow.steps) {
|
||||||
|
flowData.steps = flow.steps.map((step) => stepSerializer(step));
|
||||||
|
}
|
||||||
|
|
||||||
|
return flowData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default flowSerializer;
|
44
packages/backend/src/serializers/flow.test.js
Normal file
44
packages/backend/src/serializers/flow.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { createFlow } from '../../test/factories/flow';
|
||||||
|
import flowSerializer from './flow';
|
||||||
|
import stepSerializer from './step';
|
||||||
|
import { createStep } from '../../test/factories/step';
|
||||||
|
|
||||||
|
describe('flowSerializer', () => {
|
||||||
|
let flow, stepOne, stepTwo;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
flow = await createFlow();
|
||||||
|
|
||||||
|
stepOne = await createStep({
|
||||||
|
flowId: flow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
stepTwo = await createStep({
|
||||||
|
flowId: flow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return flow data', async () => {
|
||||||
|
const expectedPayload = {
|
||||||
|
id: flow.id,
|
||||||
|
name: flow.name,
|
||||||
|
active: flow.active,
|
||||||
|
status: flow.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(flowSerializer(flow)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return flow data with the steps', async () => {
|
||||||
|
flow.steps = [stepOne, stepTwo];
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
steps: [stepSerializer(stepOne), stepSerializer(stepTwo)],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(flowSerializer(flow)).toMatchObject(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -3,6 +3,8 @@ import roleSerializer from './role.js';
|
|||||||
import permissionSerializer from './permission.js';
|
import permissionSerializer from './permission.js';
|
||||||
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
|
import samlAuthProviderSerializer from './saml-auth-provider.ee.js';
|
||||||
import appAuthClientSerializer from './app-auth-client.js';
|
import appAuthClientSerializer from './app-auth-client.js';
|
||||||
|
import flowSerializer from './flow.js';
|
||||||
|
import stepSerializer from './step.js';
|
||||||
|
|
||||||
const serializers = {
|
const serializers = {
|
||||||
User: userSerializer,
|
User: userSerializer,
|
||||||
@@ -10,6 +12,8 @@ const serializers = {
|
|||||||
Permission: permissionSerializer,
|
Permission: permissionSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
AppAuthClient: appAuthClientSerializer,
|
||||||
|
Flow: flowSerializer,
|
||||||
|
Step: stepSerializer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default serializers;
|
export default serializers;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { createRole } from '../../test/factories/role';
|
import { createRole } from '../../test/factories/role';
|
||||||
import roleSerializer from './role';
|
import roleSerializer from './role';
|
||||||
|
import permissionSerializer from './permission';
|
||||||
import { createPermission } from '../../test/factories/permission';
|
import { createPermission } from '../../test/factories/permission';
|
||||||
|
|
||||||
describe('roleSerializer', () => {
|
describe('roleSerializer', () => {
|
||||||
@@ -40,7 +41,10 @@ describe('roleSerializer', () => {
|
|||||||
role.permissions = [permissionOne, permissionTwo];
|
role.permissions = [permissionOne, permissionTwo];
|
||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
permissions: [permissionOne, permissionTwo],
|
permissions: [
|
||||||
|
permissionSerializer(permissionOne),
|
||||||
|
permissionSerializer(permissionTwo),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(roleSerializer(role)).toMatchObject(expectedPayload);
|
expect(roleSerializer(role)).toMatchObject(expectedPayload);
|
||||||
|
15
packages/backend/src/serializers/step.js
Normal file
15
packages/backend/src/serializers/step.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const stepSerializer = (step) => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default stepSerializer;
|
27
packages/backend/src/serializers/step.test.js
Normal file
27
packages/backend/src/serializers/step.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { createStep } from '../../test/factories/step';
|
||||||
|
import stepSerializer from './step';
|
||||||
|
|
||||||
|
describe('stepSerializer', () => {
|
||||||
|
let step;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
step = await createStep();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return step data', async () => {
|
||||||
|
const expectedPayload = {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(stepSerializer(step)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -4,6 +4,8 @@ import appConfig from '../config/app';
|
|||||||
import { createUser } from '../../test/factories/user';
|
import { createUser } from '../../test/factories/user';
|
||||||
import { createPermission } from '../../test/factories/permission';
|
import { createPermission } from '../../test/factories/permission';
|
||||||
import userSerializer from './user';
|
import userSerializer from './user';
|
||||||
|
import roleSerializer from './role';
|
||||||
|
import permissionSerializer from './permission';
|
||||||
|
|
||||||
describe('userSerializer', () => {
|
describe('userSerializer', () => {
|
||||||
let user, role, permissionOne, permissionTwo;
|
let user, role, permissionOne, permissionTwo;
|
||||||
@@ -43,7 +45,7 @@ describe('userSerializer', () => {
|
|||||||
user.role = role;
|
user.role = role;
|
||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
role,
|
role: roleSerializer(role),
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
||||||
@@ -53,7 +55,10 @@ describe('userSerializer', () => {
|
|||||||
user.permissions = [permissionOne, permissionTwo];
|
user.permissions = [permissionOne, permissionTwo];
|
||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
permissions: [permissionOne, permissionTwo],
|
permissions: [
|
||||||
|
permissionSerializer(permissionOne),
|
||||||
|
permissionSerializer(permissionTwo),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
expect(userSerializer(user)).toMatchObject(expectedPayload);
|
||||||
|
32
packages/backend/test/mocks/rest/api/v1/flows/get-flow.js
Normal file
32
packages/backend/test/mocks/rest/api/v1/flows/get-flow.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const getFlowMock = async (flow, steps) => {
|
||||||
|
const data = {
|
||||||
|
active: flow.active,
|
||||||
|
id: flow.id,
|
||||||
|
name: flow.name,
|
||||||
|
status: flow.active ? 'published' : 'draft',
|
||||||
|
steps: steps.map((step) => ({
|
||||||
|
appKey: step.appKey,
|
||||||
|
iconUrl: step.iconUrl,
|
||||||
|
id: step.id,
|
||||||
|
key: step.key,
|
||||||
|
parameters: step.parameters,
|
||||||
|
position: step.position,
|
||||||
|
status: step.status,
|
||||||
|
type: step.type,
|
||||||
|
webhookUrl: step.webhookUrl,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
count: 1,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: false,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Flow',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getFlowMock;
|
Reference in New Issue
Block a user