refactor: rewrite get executions using useExecutions with RQ

This commit is contained in:
Rıdvan Akca
2024-03-12 14:33:09 +03:00
parent f07b6d105a
commit 3bc0c23e5a
7 changed files with 37 additions and 627 deletions

View File

@@ -1,70 +0,0 @@
import { raw } from 'objection';
import { DateTime } from 'luxon';
import Execution from '../../models/execution.js';
import paginate from '../../helpers/pagination.js';
const getExecutions = async (_parent, params, context) => {
const conditions = context.currentUser.can('read', 'Execution');
const filters = params.filters;
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator
? userExecutions
: allExecutions;
const selectStatusStatement = `
case
when count(*) filter (where execution_steps.status = 'failure') > 0
then 'failure'
else 'success'
end
as status
`;
const executions = executionBaseQuery
.clone()
.joinRelated('executionSteps as execution_steps')
.select('executions.*', raw(selectStatusStatement))
.groupBy('executions.id')
.orderBy('created_at', 'desc');
const computedExecutions = Execution.query()
.with('executions', executions)
.withSoftDeleted()
.withGraphFetched({
flow: {
steps: true,
},
});
if (filters?.flowId) {
computedExecutions.where('executions.flow_id', filters.flowId);
}
if (filters?.status) {
computedExecutions.where('executions.status', filters.status);
}
if (filters?.createdAt) {
const createdAtFilter = filters.createdAt;
if (createdAtFilter.from) {
const isoFromDateTime = DateTime.fromMillis(
parseInt(createdAtFilter.from, 10)
).toISO();
computedExecutions.where('executions.created_at', '>=', isoFromDateTime);
}
if (createdAtFilter.to) {
const isoToDateTime = DateTime.fromMillis(
parseInt(createdAtFilter.to, 10)
).toISO();
computedExecutions.where('executions.created_at', '<=', isoToDateTime);
}
}
return paginate(computedExecutions, params.limit, params.offset);
};
export default getExecutions;

View File

@@ -1,472 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import request from 'supertest';
import app from '../../app';
import appConfig from '../../config/app';
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
import { createRole } from '../../../test/factories/role';
import { createPermission } from '../../../test/factories/permission';
import { createUser } from '../../../test/factories/user';
import { createFlow } from '../../../test/factories/flow';
import { createStep } from '../../../test/factories/step';
import { createExecution } from '../../../test/factories/execution';
import { createExecutionStep } from '../../../test/factories/execution-step';
describe('graphQL getExecutions query', () => {
const query = `
query {
getExecutions(limit: 10, offset: 0) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
testRun
createdAt
updatedAt
status
flow {
id
name
active
steps {
iconUrl
}
}
}
}
}
}
`;
describe('and without correct permissions', () => {
it('should throw not authorized error', async () => {
const userWithoutPermissions = await createUser();
const token = createAuthTokenByUserId(userWithoutPermissions.id);
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toEqual('Not authorized!');
});
});
describe('and with correct permission', () => {
let role,
currentUser,
anotherUser,
token,
flowOne,
stepOneForFlowOne,
stepTwoForFlowOne,
executionOne,
flowTwo,
stepOneForFlowTwo,
stepTwoForFlowTwo,
executionTwo,
flowThree,
stepOneForFlowThree,
stepTwoForFlowThree,
executionThree,
expectedResponseForExecutionOne,
expectedResponseForExecutionTwo,
expectedResponseForExecutionThree;
beforeEach(async () => {
role = await createRole({
key: 'sample',
name: 'sample',
});
currentUser = await createUser({
roleId: role.id,
fullName: 'Current User',
});
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
flowOne = await createFlow({
userId: currentUser.id,
});
stepOneForFlowOne = await createStep({
flowId: flowOne.id,
});
stepTwoForFlowOne = await createStep({
flowId: flowOne.id,
});
executionOne = await createExecution({
flowId: flowOne.id,
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepOneForFlowOne.id,
status: 'success',
});
await createExecutionStep({
executionId: executionOne.id,
stepId: stepTwoForFlowOne.id,
status: 'success',
});
flowTwo = await createFlow({
userId: currentUser.id,
});
stepOneForFlowTwo = await createStep({
flowId: flowTwo.id,
});
stepTwoForFlowTwo = await createStep({
flowId: flowTwo.id,
});
executionTwo = await createExecution({
flowId: flowTwo.id,
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepOneForFlowTwo.id,
status: 'success',
});
await createExecutionStep({
executionId: executionTwo.id,
stepId: stepTwoForFlowTwo.id,
status: 'failure',
});
flowThree = await createFlow({
userId: anotherUser.id,
});
stepOneForFlowThree = await createStep({
flowId: flowThree.id,
});
stepTwoForFlowThree = await createStep({
flowId: flowThree.id,
});
executionThree = await createExecution({
flowId: flowThree.id,
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepOneForFlowThree.id,
status: 'success',
});
await createExecutionStep({
executionId: executionThree.id,
stepId: stepTwoForFlowThree.id,
status: 'failure',
});
expectedResponseForExecutionOne = {
node: {
createdAt: executionOne.createdAt.getTime().toString(),
flow: {
active: flowOne.active,
id: flowOne.id,
name: flowOne.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`,
},
],
},
id: executionOne.id,
status: 'success',
testRun: executionOne.testRun,
updatedAt: executionOne.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionTwo = {
node: {
createdAt: executionTwo.createdAt.getTime().toString(),
flow: {
active: flowTwo.active,
id: flowTwo.id,
name: flowTwo.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`,
},
],
},
id: executionTwo.id,
status: 'failure',
testRun: executionTwo.testRun,
updatedAt: executionTwo.updatedAt.getTime().toString(),
},
};
expectedResponseForExecutionThree = {
node: {
createdAt: executionThree.createdAt.getTime().toString(),
flow: {
active: flowThree.active,
id: flowThree.id,
name: flowThree.name,
steps: [
{
iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`,
},
{
iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`,
},
],
},
id: executionThree.id,
status: 'failure',
testRun: executionThree.testRun,
updatedAt: executionThree.updatedAt.getTime().toString(),
},
};
});
describe('and with isCreator condition', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: ['isCreator'],
});
});
it('should return executions data of the current user', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and without isCreator condition', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: [],
});
});
it('should return executions data of all users', async () => {
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [
expectedResponseForExecutionThree,
expectedResponseForExecutionTwo,
expectedResponseForExecutionOne,
],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with filters', () => {
beforeEach(async () => {
await createPermission({
action: 'read',
subject: 'Execution',
roleId: role.id,
conditions: [],
});
});
it('should return executions data for the specified flow', async () => {
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { flowId: "${flowOne.id}" }) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
testRun
createdAt
updatedAt
status
flow {
id
name
active
steps {
iconUrl
}
}
}
}
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return only executions data with success status', async () => {
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { status: "success" }) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
testRun
createdAt
updatedAt
status
flow {
id
name
active
steps {
iconUrl
}
}
}
}
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
it('should return only executions data within date range', async () => {
const createdAtFrom = executionOne.createdAt.getTime().toString();
const createdAtTo = executionOne.createdAt.getTime().toString();
const query = `
query {
getExecutions(limit: 10, offset: 0, filters: { createdAt: { from: "${createdAtFrom}", to: "${createdAtTo}" }}) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
testRun
createdAt
updatedAt
status
flow {
id
name
active
steps {
iconUrl
}
}
}
}
}
}
`;
const response = await request(app)
.post('/graphql')
.set('Authorization', token)
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getExecutions: {
edges: [expectedResponseForExecutionOne],
pageInfo: { currentPage: 1, totalPages: 1 },
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
});
});

View File

@@ -7,7 +7,6 @@ import getConnectedApps from './queries/get-connected-apps.js';
import getCurrentUser from './queries/get-current-user.js';
import getDynamicData from './queries/get-dynamic-data.js';
import getDynamicFields from './queries/get-dynamic-fields.js';
import getExecutions from './queries/get-executions.js';
import getFlow from './queries/get-flow.js';
import getFlows from './queries/get-flows.js';
import getInvoices from './queries/get-invoices.ee.js';
@@ -38,7 +37,6 @@ const queryResolvers = {
getCurrentUser,
getDynamicData,
getDynamicFields,
getExecutions,
getFlow,
getFlows,
getInvoices,

View File

@@ -13,11 +13,6 @@ type Query {
name: String
): FlowConnection
getStepWithTestExecutions(stepId: String!): [Step]
getExecutions(
limit: Int!
offset: Int!
filters: ExecutionFiltersInput
): ExecutionConnection
getDynamicData(
stepId: String!
key: String!
@@ -301,15 +296,6 @@ type Flow {
status: FlowStatus
}
type Execution {
id: String
testRun: Boolean
createdAt: String
updatedAt: String
status: String
flow: Flow
}
type SamlAuthProvider {
id: String
name: String
@@ -609,19 +595,10 @@ type PageInfo {
totalPages: Int!
}
type ExecutionEdge {
node: Execution
}
type ExecutionStepEdge {
node: ExecutionStep
}
type ExecutionConnection {
edges: [ExecutionEdge]
pageInfo: PageInfo
}
type ExecutionStepConnection {
edges: [ExecutionStepEdge]
pageInfo: PageInfo
@@ -781,17 +758,6 @@ type Notification {
description: String
}
input ExecutionCreatedAtFilterInput {
from: String
to: String
}
input ExecutionFiltersInput {
flowId: String
createdAt: ExecutionCreatedAtFilterInput
status: String
}
schema {
query: Query
mutation: Mutation

View File

@@ -1,28 +0,0 @@
import { gql } from '@apollo/client';
export const GET_EXECUTIONS = gql`
query GetExecutions($limit: Int!, $offset: Int!) {
getExecutions(limit: $limit, offset: $offset) {
pageInfo {
currentPage
totalPages
}
edges {
node {
id
testRun
createdAt
updatedAt
status
flow {
id
name
active
steps {
iconUrl
}
}
}
}
}
}
`;

View File

@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
export default function useExecutions({ page }, { refetchInterval } = {}) {
const query = useQuery({
queryKey: ['executions', page],
queryFn: async ({ signal }) => {
const { data } = await api.get(`/v1/executions`, {
params: {
page,
},
signal,
});
return data;
},
refetchInterval,
});
return query;
}

View File

@@ -1,39 +1,33 @@
import * as React from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import Pagination from '@mui/material/Pagination';
import PaginationItem from '@mui/material/PaginationItem';
import NoResultFound from 'components/NoResultFound';
import ExecutionRow from 'components/ExecutionRow';
import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import useFormatMessage from 'hooks/useFormatMessage';
import { GET_EXECUTIONS } from 'graphql/queries/get-executions';
const EXECUTION_PER_PAGE = 10;
const getLimitAndOffset = (page) => ({
limit: EXECUTION_PER_PAGE,
offset: (page - 1) * EXECUTION_PER_PAGE,
});
import useExecutions from 'hooks/useExecutions';
export default function Executions() {
const formatMessage = useFormatMessage();
const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '', 10) || 1;
const { data, refetch, loading } = useQuery(GET_EXECUTIONS, {
variables: getLimitAndOffset(page),
fetchPolicy: 'cache-and-network',
pollInterval: 5000,
});
const getExecutions = data?.getExecutions || {};
const { pageInfo, edges } = getExecutions;
React.useEffect(() => {
refetch(getLimitAndOffset(page));
}, [refetch, page]);
const executions = edges?.map(({ node }) => node);
const { data, isLoading: isExecutionsLoading } = useExecutions(
{ page: page },
{ refetchInterval: 5000 },
);
const { data: executions, meta: pageInfo } = data || {};
const hasExecutions = executions?.length;
return (
<Box sx={{ py: 3 }}>
<Container>
@@ -52,18 +46,18 @@ export default function Executions() {
<Divider sx={{ mt: [2, 0], mb: 2 }} />
{loading && (
{isExecutionsLoading && (
<CircularProgress
data-test="executions-loader"
sx={{ display: 'block', margin: '20px auto' }}
/>
)}
{!loading && !hasExecutions && (
{!isExecutionsLoading && !hasExecutions && (
<NoResultFound text={formatMessage('executions.noExecutions')} />
)}
{!loading &&
{!isExecutionsLoading &&
executions?.map((execution) => (
<ExecutionRow key={execution.id} execution={execution} />
))}