Skip to content

Commit 79d5988

Browse files
authored
feat(orchestrator): support pagination for /instances and /overview (#1313)
* feat: support pagination for /instances and /overview * refactor: isolate pagination logics from /v1 endpoints
1 parent df868aa commit 79d5988

File tree

15 files changed

+459
-67
lines changed

15 files changed

+459
-67
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Pagination } from '../types/pagination';
2+
3+
export function buildGraphQlQuery(args: {
4+
type: 'ProcessDefinitions' | 'ProcessInstances' | 'Jobs';
5+
queryBody: string;
6+
whereClause?: string;
7+
pagination?: Pagination;
8+
}): string {
9+
let query = `{${args.type}`;
10+
11+
if (args.whereClause || args.pagination) {
12+
query += ` (`;
13+
14+
if (args.whereClause) {
15+
query += `where: {${args.whereClause}}`;
16+
if (args.pagination) {
17+
query += `, `;
18+
}
19+
}
20+
if (args.pagination) {
21+
if (args.pagination.sortField) {
22+
query += `orderBy: {${
23+
args.pagination.sortField
24+
}: ${args.pagination.order?.toUpperCase()}}, `;
25+
}
26+
query += `pagination: {limit: ${args.pagination.limit} , offset: ${args.pagination.offset}}`;
27+
}
28+
29+
query += `) `;
30+
}
31+
query += ` {${args.queryBody} } }`;
32+
33+
return query;
34+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { buildPagination } from './types/pagination';
2+
3+
describe('buildPagination()', () => {
4+
it('should build the correct pagination obj when no query parameters are passed', () => {
5+
const mockRequest: any = {
6+
query: {},
7+
};
8+
expect(buildPagination(mockRequest)).toEqual({
9+
limit: 10,
10+
offset: 0,
11+
order: 'ASC',
12+
sortField: undefined,
13+
});
14+
});
15+
it('should build the correct pagination obj when partial query parameters are passed', () => {
16+
const mockRequest: any = {
17+
query: {
18+
orderBy: 'lastUpdated',
19+
},
20+
};
21+
expect(buildPagination(mockRequest)).toEqual({
22+
limit: 10,
23+
offset: 0,
24+
order: 'ASC',
25+
sortField: 'lastUpdated',
26+
});
27+
});
28+
it('should build the correct pagination obj when all query parameters are passed', () => {
29+
const mockRequest: any = {
30+
query: {
31+
page: 1,
32+
pageSize: 50,
33+
orderBy: 'lastUpdated',
34+
orderDirection: 'DESC',
35+
},
36+
};
37+
expect(buildPagination(mockRequest)).toEqual({
38+
limit: 50,
39+
offset: 1,
40+
order: 'DESC',
41+
sortField: 'lastUpdated',
42+
});
43+
});
44+
it('should build the correct pagination obj when non numeric value passed to number fields', () => {
45+
const mockRequest: any = {
46+
query: {
47+
page: 'abc',
48+
pageSize: 'cde',
49+
},
50+
};
51+
expect(buildPagination(mockRequest)).toEqual({
52+
limit: 10,
53+
offset: 0,
54+
order: 'ASC',
55+
sortField: undefined,
56+
});
57+
});
58+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { buildGraphQlQuery } from './helpers/queryBuilder';
2+
import { Pagination } from './types/pagination';
3+
4+
describe('GraphQL query builder', () => {
5+
it('should return properly formatted graphQL query when where clause and pagination are present', () => {
6+
const expectedQuery: string =
7+
'{ProcessInstances (where: {processId: {isNull: false}}, orderBy: {lastUpdate: DESC}, pagination: {limit: 5 , offset: 2}) {id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }';
8+
const pagination: Pagination = {
9+
offset: 2,
10+
limit: 5,
11+
order: 'DESC',
12+
sortField: 'lastUpdate',
13+
};
14+
expect(
15+
buildGraphQlQuery({
16+
type: 'ProcessInstances',
17+
queryBody:
18+
'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}',
19+
whereClause: 'processId: {isNull: false}',
20+
pagination,
21+
}),
22+
).toEqual(expectedQuery);
23+
});
24+
25+
it('should return properly formatted graphQL query when where clause is present', () => {
26+
const expectedQuery: string =
27+
'{ProcessInstances (where: {processId: {isNull: false}}) {id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }';
28+
expect(
29+
buildGraphQlQuery({
30+
type: 'ProcessInstances',
31+
queryBody:
32+
'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}',
33+
whereClause: 'processId: {isNull: false}',
34+
}),
35+
).toEqual(expectedQuery);
36+
});
37+
38+
it('should return properly formatted graphQL query when where clause is NOT present', () => {
39+
const expectedQuery: string =
40+
'{ProcessInstances {id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }';
41+
expect(
42+
buildGraphQlQuery({
43+
type: 'ProcessInstances',
44+
queryBody:
45+
'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}',
46+
}),
47+
).toEqual(expectedQuery);
48+
});
49+
});

plugins/orchestrator-backend/src/service/DataIndexService.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
} from '@janus-idp/backstage-plugin-orchestrator-common';
1313

1414
import { ErrorBuilder } from '../helpers/errorBuilder';
15+
import { buildGraphQlQuery } from '../helpers/queryBuilder';
16+
import { Pagination } from '../types/pagination';
17+
import { FETCH_PROCESS_INSTANCES_SORT_FIELD } from './constants';
1518

1619
export class DataIndexService {
1720
private client: Client;
@@ -89,23 +92,18 @@ export class DataIndexService {
8992
return processDefinitions[0];
9093
}
9194

92-
public async getWorkflowInfos(): Promise<WorkflowInfo[]> {
93-
const QUERY = `
94-
query ProcessDefinitions {
95-
ProcessDefinitions {
96-
id
97-
name
98-
version
99-
type
100-
endpoint
101-
serviceUrl
102-
source
103-
}
104-
}
105-
`;
106-
95+
public async getWorkflowInfos(
96+
pagination?: Pagination,
97+
): Promise<WorkflowInfo[]> {
10798
this.logger.info(`getWorkflowInfos() called: ${this.dataIndexUrl}`);
108-
const result = await this.client.query(QUERY, {});
99+
100+
const graphQlQuery = buildGraphQlQuery({
101+
type: 'ProcessDefinitions',
102+
queryBody: 'id, name, version, type, endpoint, serviceUrl, source',
103+
pagination,
104+
});
105+
this.logger.debug(`GraphQL query: ${graphQlQuery}`);
106+
const result = await this.client.query(graphQlQuery, {});
109107

110108
this.logger.debug(
111109
`Get workflow definitions result: ${JSON.stringify(result)}`,
@@ -121,10 +119,19 @@ export class DataIndexService {
121119
return result.data.ProcessDefinitions;
122120
}
123121

124-
public async fetchProcessInstances(): Promise<ProcessInstance[] | undefined> {
125-
const graphQlQuery =
126-
'{ ProcessInstances ( orderBy: { start: ASC }, where: {processId: {isNull: false} } ) { id, processName, processId, businessKey, state, start, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }';
127-
122+
public async fetchProcessInstances(
123+
pagination?: Pagination,
124+
): Promise<ProcessInstance[] | undefined> {
125+
if (pagination) pagination.sortField ??= FETCH_PROCESS_INSTANCES_SORT_FIELD;
126+
127+
const graphQlQuery = buildGraphQlQuery({
128+
type: 'ProcessInstances',
129+
queryBody:
130+
'id, processName, processId, businessKey, state, start, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}',
131+
whereClause: 'processId: {isNull: false}',
132+
pagination,
133+
});
134+
this.logger.debug(`GraphQL query: ${graphQlQuery}`);
128135
const result = await this.client.query(graphQlQuery, {});
129136

130137
this.logger.debug(
@@ -147,6 +154,24 @@ export class DataIndexService {
147154
return processInstances;
148155
}
149156

157+
public async getProcessInstancesTotalCount(): Promise<number> {
158+
const graphQlQuery = buildGraphQlQuery({
159+
type: 'ProcessInstances',
160+
queryBody: 'id',
161+
});
162+
this.logger.debug(`GraphQL query: ${graphQlQuery}`);
163+
const result = await this.client.query(graphQlQuery, {});
164+
165+
if (result.error) {
166+
this.logger.error(`Error when fetching instances: ${result.error}`);
167+
throw result.error;
168+
}
169+
170+
const idArr = result.data.ProcessInstances as ProcessInstance[];
171+
172+
return Promise.resolve(idArr.length);
173+
}
174+
150175
private async getWorkflowDefinitionFromInstance(instance: ProcessInstance) {
151176
const workflowInfo = await this.getWorkflowDefinition(instance.processId);
152177
if (!workflowInfo?.source) {

plugins/orchestrator-backend/src/service/SonataFlowService.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import { spawn } from 'child_process';
2222
import { join, resolve } from 'path';
2323

24+
import { Pagination } from '../types/pagination';
2425
import { DataIndexService } from './DataIndexService';
2526
import { executeWithRetry } from './Helper';
2627

@@ -143,11 +144,11 @@ export class SonataFlowService {
143144
return undefined;
144145
}
145146

146-
public async fetchWorkflowOverviews(): Promise<
147-
WorkflowOverview[] | undefined
148-
> {
147+
public async fetchWorkflowOverviews(
148+
pagination?: Pagination,
149+
): Promise<WorkflowOverview[] | undefined> {
149150
try {
150-
const workflowInfos = await this.dataIndex.getWorkflowInfos();
151+
const workflowInfos = await this.dataIndex.getWorkflowInfos(pagination);
151152
if (!workflowInfos?.length) {
152153
return [];
153154
}

plugins/orchestrator-backend/src/service/api/v2.test.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WorkflowOverviewListResultDTO,
55
} from '@janus-idp/backstage-plugin-orchestrator-common';
66

7+
import { buildPagination } from '../../types/pagination';
78
import { SonataFlowService } from '../SonataFlowService';
89
import { mapToWorkflowOverviewDTO } from './mapping/V2Mappings';
910
import {
@@ -48,6 +49,14 @@ describe('getWorkflowOverview', () => {
4849

4950
it('0 items in workflow overview list', async () => {
5051
// Arrange
52+
const mockRequest: any = {
53+
query: {
54+
page: 1,
55+
pageSize: 50,
56+
orderBy: 'lastUpdated',
57+
orderDirection: 'DESC',
58+
},
59+
};
5160
const mockOverviewsV1 = {
5261
items: [],
5362
};
@@ -59,6 +68,7 @@ describe('getWorkflowOverview', () => {
5968
// Act
6069
const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview(
6170
mockSonataFlowService,
71+
buildPagination(mockRequest),
6272
);
6373

6474
// Assert
@@ -67,15 +77,18 @@ describe('getWorkflowOverview', () => {
6777
mapToWorkflowOverviewDTO(item),
6878
),
6979
paginationInfo: {
70-
limit: 0,
71-
offset: 0,
80+
page: 1,
81+
pageSize: 50,
7282
totalCount: mockOverviewsV1.items.length,
7383
},
7484
});
7585
});
7686

7787
it('1 item in workflow overview list', async () => {
7888
// Arrange
89+
const mockRequest: any = {
90+
query: {},
91+
};
7992
const mockOverviewsV1 = generateTestWorkflowOverviewList(1, {});
8093

8194
(
@@ -85,6 +98,7 @@ describe('getWorkflowOverview', () => {
8598
// Act
8699
const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview(
87100
mockSonataFlowService,
101+
buildPagination(mockRequest),
88102
);
89103

90104
// Assert
@@ -93,15 +107,23 @@ describe('getWorkflowOverview', () => {
93107
mapToWorkflowOverviewDTO(item),
94108
),
95109
paginationInfo: {
96-
limit: 0,
97-
offset: 0,
110+
page: 0,
111+
pageSize: 10,
98112
totalCount: mockOverviewsV1.items.length,
99113
},
100114
});
101115
});
102116

103117
it('many items in workflow overview list', async () => {
104118
// Arrange
119+
const mockRequest: any = {
120+
query: {
121+
page: 1,
122+
pageSize: 50,
123+
orderBy: 'lastUpdated',
124+
orderDirection: 'DESC',
125+
},
126+
};
105127
const mockOverviewsV1 = generateTestWorkflowOverviewList(100, {});
106128

107129
(
@@ -111,6 +133,7 @@ describe('getWorkflowOverview', () => {
111133
// Act
112134
const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview(
113135
mockSonataFlowService,
136+
buildPagination(mockRequest),
114137
);
115138

116139
// Assert
@@ -119,21 +142,27 @@ describe('getWorkflowOverview', () => {
119142
mapToWorkflowOverviewDTO(item),
120143
),
121144
paginationInfo: {
122-
limit: 0,
123-
offset: 0,
145+
page: 1,
146+
pageSize: 50,
124147
totalCount: mockOverviewsV1.items.length,
125148
},
126149
});
127150
});
128151

129152
it('undefined workflow overview list', async () => {
130153
// Arrange
154+
const mockRequest: any = {
155+
query: {},
156+
};
131157
(
132158
mockSonataFlowService.fetchWorkflowOverviews as jest.Mock
133159
).mockRejectedValue(new Error('no workflow overview'));
134160

135161
// Act
136-
const promise = V2.getWorkflowsOverview(mockSonataFlowService);
162+
const promise = V2.getWorkflowsOverview(
163+
mockSonataFlowService,
164+
buildPagination(mockRequest),
165+
);
137166

138167
// Assert
139168
await expect(promise).rejects.toThrow('no workflow overview');

0 commit comments

Comments
 (0)