From 9ce805a10fa86a7bcdee8e0280d8ebcbbd798e1f Mon Sep 17 00:00:00 2001 From: Jude Niroshan Date: Wed, 6 Mar 2024 18:48:25 +0100 Subject: [PATCH 1/2] feat: support pagination for /instances and /overview --- .../src/helpers/queryBuilder.ts | 34 ++++++++++ .../src/pagination.test.ts | 58 +++++++++++++++++ .../src/queryBuilder.test.ts | 49 ++++++++++++++ .../src/service/DataIndexService.ts | 65 +++++++++++++------ .../src/service/SonataFlowService.ts | 9 +-- .../src/service/api/v1.ts | 8 ++- .../src/service/api/v2.test.ts | 43 ++++++++++-- .../src/service/api/v2.ts | 28 ++++++-- .../src/service/constants.ts | 5 ++ .../src/service/router.ts | 23 ++++--- .../src/types/pagination.ts | 32 +++++++++ .../src/auto-generated/api/definition.ts | 56 ++++++++++++++-- .../src/auto-generated/api/models/schema.ts | 23 +++++-- .../auto-generated/docs/index.adoc/index.adoc | 64 ++++++++++++++++-- .../src/openapi/openapi.yaml | 40 ++++++++++-- .../orchestrator-common/src/openapi/types.ts | 3 +- 16 files changed, 469 insertions(+), 71 deletions(-) create mode 100644 plugins/orchestrator-backend/src/helpers/queryBuilder.ts create mode 100644 plugins/orchestrator-backend/src/pagination.test.ts create mode 100644 plugins/orchestrator-backend/src/queryBuilder.test.ts create mode 100644 plugins/orchestrator-backend/src/types/pagination.ts diff --git a/plugins/orchestrator-backend/src/helpers/queryBuilder.ts b/plugins/orchestrator-backend/src/helpers/queryBuilder.ts new file mode 100644 index 0000000000..5f0a147f9f --- /dev/null +++ b/plugins/orchestrator-backend/src/helpers/queryBuilder.ts @@ -0,0 +1,34 @@ +import { Pagination } from '../types/pagination'; + +export function buildGraphQlQuery(args: { + type: 'ProcessDefinitions' | 'ProcessInstances' | 'Jobs'; + queryBody: string; + whereClause?: string; + pagination?: Pagination; +}): string { + let query = `{${args.type}`; + + if (args.whereClause || args.pagination) { + query += ` (`; + + if (args.whereClause) { + query += `where: {${args.whereClause}}`; + if (args.pagination) { + query += `, `; + } + } + if (args.pagination) { + if (args.pagination.sortField) { + query += `orderBy: {${ + args.pagination.sortField + }: ${args.pagination.order?.toUpperCase()}}, `; + } + query += `pagination: {limit: ${args.pagination.limit} , offset: ${args.pagination.offset}}`; + } + + query += `) `; + } + query += ` {${args.queryBody} } }`; + + return query; +} diff --git a/plugins/orchestrator-backend/src/pagination.test.ts b/plugins/orchestrator-backend/src/pagination.test.ts new file mode 100644 index 0000000000..f571f2ebec --- /dev/null +++ b/plugins/orchestrator-backend/src/pagination.test.ts @@ -0,0 +1,58 @@ +import { buildPagination } from './types/pagination'; + +describe('buildPagination()', () => { + it('should build the correct pagination obj when no query parameters are passed', () => { + const mockRequest: any = { + query: {}, + }; + expect(buildPagination(mockRequest)).toEqual({ + limit: 10, + offset: 0, + order: 'ASC', + sortField: undefined, + }); + }); + it('should build the correct pagination obj when partial query parameters are passed', () => { + const mockRequest: any = { + query: { + orderBy: 'lastUpdated', + }, + }; + expect(buildPagination(mockRequest)).toEqual({ + limit: 10, + offset: 0, + order: 'ASC', + sortField: 'lastUpdated', + }); + }); + it('should build the correct pagination obj when all query parameters are passed', () => { + const mockRequest: any = { + query: { + page: 1, + pageSize: 50, + orderBy: 'lastUpdated', + orderDirection: 'DESC', + }, + }; + expect(buildPagination(mockRequest)).toEqual({ + limit: 50, + offset: 1, + order: 'DESC', + sortField: 'lastUpdated', + }); + }); + it('should build the correct pagination obj when non numeric value passed to number fields', () => { + const mockRequest: any = { + query: { + page: 'abc', + pageSize: 'cde', + }, + }; + expect(buildPagination(mockRequest)).toEqual({ + limit: 10, + offset: 0, + order: 'ASC', + sortField: undefined, + }); + }); +}); diff --git a/plugins/orchestrator-backend/src/queryBuilder.test.ts b/plugins/orchestrator-backend/src/queryBuilder.test.ts new file mode 100644 index 0000000000..84084837c3 --- /dev/null +++ b/plugins/orchestrator-backend/src/queryBuilder.test.ts @@ -0,0 +1,49 @@ +import { buildGraphQlQuery } from './helpers/queryBuilder'; +import { Pagination } from './types/pagination'; + +describe('GraphQL query builder', () => { + it('should return properly formatted graphQL query when where clause and pagination are present', () => { + const expectedQuery: string = + '{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} } }'; + const pagination: Pagination = { + offset: 2, + limit: 5, + order: 'DESC', + sortField: 'lastUpdate', + }; + expect( + buildGraphQlQuery({ + type: 'ProcessInstances', + queryBody: + 'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}', + whereClause: 'processId: {isNull: false}', + pagination, + }), + ).toEqual(expectedQuery); + }); + + it('should return properly formatted graphQL query when where clause is present', () => { + const expectedQuery: string = + '{ProcessInstances (where: {processId: {isNull: false}}) {id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }'; + expect( + buildGraphQlQuery({ + type: 'ProcessInstances', + queryBody: + 'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}', + whereClause: 'processId: {isNull: false}', + }), + ).toEqual(expectedQuery); + }); + + it('should return properly formatted graphQL query when where clause is NOT present', () => { + const expectedQuery: string = + '{ProcessInstances {id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }'; + expect( + buildGraphQlQuery({ + type: 'ProcessInstances', + queryBody: + 'id, processName, processId, state, start, lastUpdate, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}', + }), + ).toEqual(expectedQuery); + }); +}); diff --git a/plugins/orchestrator-backend/src/service/DataIndexService.ts b/plugins/orchestrator-backend/src/service/DataIndexService.ts index 4ba3f61fbb..80ef7a7a4d 100644 --- a/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -12,6 +12,9 @@ import { } from '@janus-idp/backstage-plugin-orchestrator-common'; import { ErrorBuilder } from '../helpers/errorBuilder'; +import { buildGraphQlQuery } from '../helpers/queryBuilder'; +import { Pagination } from '../types/pagination'; +import { FETCH_PROCESS_INSTANCES_SORT_FIELD } from './constants'; export class DataIndexService { private client: Client; @@ -89,23 +92,18 @@ export class DataIndexService { return processDefinitions[0]; } - public async getWorkflowInfos(): Promise { - const QUERY = ` - query ProcessDefinitions { - ProcessDefinitions { - id - name - version - type - endpoint - serviceUrl - source - } - } - `; - + public async getWorkflowInfos( + pagination: Pagination, + ): Promise { this.logger.info(`getWorkflowInfos() called: ${this.dataIndexUrl}`); - const result = await this.client.query(QUERY, {}); + + const graphQlQuery = buildGraphQlQuery({ + type: 'ProcessDefinitions', + queryBody: 'id, name, version, type, endpoint, serviceUrl, source', + pagination, + }); + this.logger.debug(`GraphQL query: ${graphQlQuery}`); + const result = await this.client.query(graphQlQuery, {}); this.logger.debug( `Get workflow definitions result: ${JSON.stringify(result)}`, @@ -121,10 +119,19 @@ export class DataIndexService { return result.data.ProcessDefinitions; } - public async fetchProcessInstances(): Promise { - const graphQlQuery = - '{ ProcessInstances ( orderBy: { start: ASC }, where: {processId: {isNull: false} } ) { id, processName, processId, businessKey, state, start, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey} } }'; - + public async fetchProcessInstances( + pagination: Pagination, + ): Promise { + pagination.sortField ??= FETCH_PROCESS_INSTANCES_SORT_FIELD; + + const graphQlQuery = buildGraphQlQuery({ + type: 'ProcessInstances', + queryBody: + 'id, processName, processId, businessKey, state, start, end, nodes { id }, variables, parentProcessInstance {id, processName, businessKey}', + whereClause: 'processId: {isNull: false}', + pagination, + }); + this.logger.debug(`GraphQL query: ${graphQlQuery}`); const result = await this.client.query(graphQlQuery, {}); this.logger.debug( @@ -147,6 +154,24 @@ export class DataIndexService { return processInstances; } + public async getProcessInstancesTotalCount(): Promise { + const graphQlQuery = buildGraphQlQuery({ + type: 'ProcessInstances', + queryBody: 'id', + }); + this.logger.debug(`GraphQL query: ${graphQlQuery}`); + const result = await this.client.query(graphQlQuery, {}); + + if (result.error) { + this.logger.error(`Error when fetching instances: ${result.error}`); + throw result.error; + } + + const idArr = result.data.ProcessInstances as ProcessInstance[]; + + return Promise.resolve(idArr.length); + } + private async getWorkflowDefinitionFromInstance(instance: ProcessInstance) { const workflowInfo = await this.getWorkflowDefinition(instance.processId); if (!workflowInfo?.source) { diff --git a/plugins/orchestrator-backend/src/service/SonataFlowService.ts b/plugins/orchestrator-backend/src/service/SonataFlowService.ts index b5fd062cc0..e6c7498cd2 100644 --- a/plugins/orchestrator-backend/src/service/SonataFlowService.ts +++ b/plugins/orchestrator-backend/src/service/SonataFlowService.ts @@ -21,6 +21,7 @@ import { import { spawn } from 'child_process'; import { join, resolve } from 'path'; +import { Pagination } from '../types/pagination'; import { DataIndexService } from './DataIndexService'; import { executeWithRetry } from './Helper'; @@ -143,11 +144,11 @@ export class SonataFlowService { return undefined; } - public async fetchWorkflowOverviews(): Promise< - WorkflowOverview[] | undefined - > { + public async fetchWorkflowOverviews( + pagination: Pagination, + ): Promise { try { - const workflowInfos = await this.dataIndex.getWorkflowInfos(); + const workflowInfos = await this.dataIndex.getWorkflowInfos(pagination); if (!workflowInfos?.length) { return []; } diff --git a/plugins/orchestrator-backend/src/service/api/v1.ts b/plugins/orchestrator-backend/src/service/api/v1.ts index 7540796ca8..c06ea441ec 100644 --- a/plugins/orchestrator-backend/src/service/api/v1.ts +++ b/plugins/orchestrator-backend/src/service/api/v1.ts @@ -10,6 +10,7 @@ import { WorkflowOverviewListResult, } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { Pagination } from '../../types/pagination'; import { DataIndexService } from '../DataIndexService'; import { retryAsyncFunction } from '../Helper'; import { SonataFlowService } from '../SonataFlowService'; @@ -20,8 +21,10 @@ const FETCH_INSTANCE_RETRY_DELAY_MS = 1000; export namespace V1 { export async function getWorkflowsOverview( sonataFlowService: SonataFlowService, + pagination: Pagination, ): Promise { - const overviews = await sonataFlowService.fetchWorkflowOverviews(); + const overviews = + await sonataFlowService.fetchWorkflowOverviews(pagination); if (!overviews) { throw new Error("Couldn't fetch workflow overviews"); } @@ -76,8 +79,9 @@ export namespace V1 { export async function getInstances( dataIndexService: DataIndexService, + pagination: Pagination, ): Promise { - const instances = await dataIndexService.fetchProcessInstances(); + const instances = await dataIndexService.fetchProcessInstances(pagination); if (!instances) { throw new Error("Couldn't fetch process instances"); diff --git a/plugins/orchestrator-backend/src/service/api/v2.test.ts b/plugins/orchestrator-backend/src/service/api/v2.test.ts index c28dedca86..69323eebfc 100644 --- a/plugins/orchestrator-backend/src/service/api/v2.test.ts +++ b/plugins/orchestrator-backend/src/service/api/v2.test.ts @@ -4,6 +4,7 @@ import { WorkflowOverviewListResultDTO, } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { buildPagination } from '../../types/pagination'; import { SonataFlowService } from '../SonataFlowService'; import { mapToWorkflowOverviewDTO } from './mapping/V2Mappings'; import { @@ -48,6 +49,14 @@ describe('getWorkflowOverview', () => { it('0 items in workflow overview list', async () => { // Arrange + const mockRequest: any = { + query: { + page: 1, + pageSize: 50, + orderBy: 'lastUpdated', + orderDirection: 'DESC', + }, + }; const mockOverviewsV1 = { items: [], }; @@ -59,6 +68,7 @@ describe('getWorkflowOverview', () => { // Act const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview( mockSonataFlowService, + buildPagination(mockRequest), ); // Assert @@ -67,8 +77,8 @@ describe('getWorkflowOverview', () => { mapToWorkflowOverviewDTO(item), ), paginationInfo: { - limit: 0, - offset: 0, + page: 1, + pageSize: 50, totalCount: mockOverviewsV1.items.length, }, }); @@ -76,6 +86,9 @@ describe('getWorkflowOverview', () => { it('1 item in workflow overview list', async () => { // Arrange + const mockRequest: any = { + query: {}, + }; const mockOverviewsV1 = generateTestWorkflowOverviewList(1, {}); ( @@ -85,6 +98,7 @@ describe('getWorkflowOverview', () => { // Act const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview( mockSonataFlowService, + buildPagination(mockRequest), ); // Assert @@ -93,8 +107,8 @@ describe('getWorkflowOverview', () => { mapToWorkflowOverviewDTO(item), ), paginationInfo: { - limit: 0, - offset: 0, + page: 0, + pageSize: 10, totalCount: mockOverviewsV1.items.length, }, }); @@ -102,6 +116,14 @@ describe('getWorkflowOverview', () => { it('many items in workflow overview list', async () => { // Arrange + const mockRequest: any = { + query: { + page: 1, + pageSize: 50, + orderBy: 'lastUpdated', + orderDirection: 'DESC', + }, + }; const mockOverviewsV1 = generateTestWorkflowOverviewList(100, {}); ( @@ -111,6 +133,7 @@ describe('getWorkflowOverview', () => { // Act const result: WorkflowOverviewListResultDTO = await V2.getWorkflowsOverview( mockSonataFlowService, + buildPagination(mockRequest), ); // Assert @@ -119,8 +142,8 @@ describe('getWorkflowOverview', () => { mapToWorkflowOverviewDTO(item), ), paginationInfo: { - limit: 0, - offset: 0, + page: 1, + pageSize: 50, totalCount: mockOverviewsV1.items.length, }, }); @@ -128,12 +151,18 @@ describe('getWorkflowOverview', () => { it('undefined workflow overview list', async () => { // Arrange + const mockRequest: any = { + query: {}, + }; ( mockSonataFlowService.fetchWorkflowOverviews as jest.Mock ).mockRejectedValue(new Error('no workflow overview')); // Act - const promise = V2.getWorkflowsOverview(mockSonataFlowService); + const promise = V2.getWorkflowsOverview( + mockSonataFlowService, + buildPagination(mockRequest), + ); // Assert await expect(promise).rejects.toThrow('no workflow overview'); diff --git a/plugins/orchestrator-backend/src/service/api/v2.ts b/plugins/orchestrator-backend/src/service/api/v2.ts index 8aa77a6279..047bac153a 100644 --- a/plugins/orchestrator-backend/src/service/api/v2.ts +++ b/plugins/orchestrator-backend/src/service/api/v2.ts @@ -5,7 +5,7 @@ import { AssessedProcessInstanceDTO, ExecuteWorkflowRequestDTO, ExecuteWorkflowResponseDTO, - ProcessInstancesDTO, + ProcessInstanceListResultDTO, ProcessInstanceState, WorkflowDataDTO, WorkflowDTO, @@ -14,6 +14,7 @@ import { WorkflowRunStatusDTO, } from '@janus-idp/backstage-plugin-orchestrator-common'; +import { Pagination } from '../../types/pagination'; import { DataIndexService } from '../DataIndexService'; import { SonataFlowService } from '../SonataFlowService'; import { @@ -28,13 +29,17 @@ import { V1 } from './v1'; export namespace V2 { export async function getWorkflowsOverview( sonataFlowService: SonataFlowService, + pagination: Pagination, ): Promise { - const overviewsV1 = await V1.getWorkflowsOverview(sonataFlowService); + const overviewsV1 = await V1.getWorkflowsOverview( + sonataFlowService, + pagination, + ); const result: WorkflowOverviewListResultDTO = { overviews: overviewsV1.items.map(item => mapToWorkflowOverviewDTO(item)), paginationInfo: { - limit: 0, - offset: 0, + pageSize: pagination.limit, + page: pagination.offset, totalCount: overviewsV1.items?.length ?? 0, }, }; @@ -80,10 +85,19 @@ export namespace V2 { export async function getInstances( dataIndexService: DataIndexService, - ): Promise { - const instances = await V1.getInstances(dataIndexService); - const result = instances.map(def => mapToProcessInstanceDTO(def)); + pagination: Pagination, + ): Promise { + const instances = await V1.getInstances(dataIndexService, pagination); + const totalCount = await dataIndexService.getProcessInstancesTotalCount(); + const result: ProcessInstanceListResultDTO = { + items: instances.map(def => mapToProcessInstanceDTO(def)), + paginationInfo: { + pageSize: pagination.limit, + page: pagination.offset, + totalCount: totalCount, + }, + }; return result; } diff --git a/plugins/orchestrator-backend/src/service/constants.ts b/plugins/orchestrator-backend/src/service/constants.ts index c90564c40d..ec478c22aa 100644 --- a/plugins/orchestrator-backend/src/service/constants.ts +++ b/plugins/orchestrator-backend/src/service/constants.ts @@ -1,3 +1,8 @@ export const WORKFLOW_DATA_KEY = 'workflowdata'; export const INTERNAL_SERVER_ERROR_MESSAGE = 'internal server error'; +export const DEFAULT_PAGE_NUMBER = 0; +export const DEFAULT_PAGE_SIZE = 10; +export const DEFAULT_SORT_FIELD = undefined; +export const DEFAULT_SORT_ORDER = 'ASC'; +export const FETCH_PROCESS_INSTANCES_SORT_FIELD = 'start'; diff --git a/plugins/orchestrator-backend/src/service/router.ts b/plugins/orchestrator-backend/src/service/router.ts index 89962b6528..f327210ecb 100644 --- a/plugins/orchestrator-backend/src/service/router.ts +++ b/plugins/orchestrator-backend/src/service/router.ts @@ -17,6 +17,7 @@ import { } from '@janus-idp/backstage-plugin-orchestrator-common'; import { RouterArgs } from '../routerWrapper'; +import { buildPagination } from '../types/pagination'; import { V1 } from './api/v1'; import { V2 } from './api/v2'; import { CloudEventService } from './CloudEventService'; @@ -144,8 +145,11 @@ function setupInternalRoutes( api: OpenAPIBackend, services: Services, ) { - router.get('/workflows/overview', async (_c, res) => { - await V1.getWorkflowsOverview(services.sonataFlowService) + router.get('/workflows/overview', async (req, res) => { + await V1.getWorkflowsOverview( + services.sonataFlowService, + buildPagination(req), + ) .then(result => res.status(200).json(result)) .catch(error => { res @@ -157,8 +161,11 @@ function setupInternalRoutes( // v2 api.register( 'getWorkflowsOverview', - async (_c, _req, res: express.Response, next) => { - await V2.getWorkflowsOverview(services.sonataFlowService) + async (_c, req, res: express.Response, next) => { + await V2.getWorkflowsOverview( + services.sonataFlowService, + buildPagination(req), + ) .then(result => res.json(result)) .catch(error => { res @@ -344,8 +351,8 @@ function setupInternalRoutes( }, ); - router.get('/instances', async (_, res) => { - await V1.getInstances(services.dataIndexService) + router.get('/instances', async (req, res) => { + await V1.getInstances(services.dataIndexService, buildPagination(req)) .then(result => res.status(200).json(result)) .catch(error => { res @@ -357,8 +364,8 @@ function setupInternalRoutes( // v2 api.register( 'getInstances', - async (_c, _req: express.Request, res: express.Response, next) => { - await V2.getInstances(services.dataIndexService) + async (_c, req: express.Request, res: express.Response, next) => { + await V2.getInstances(services.dataIndexService, buildPagination(req)) .then(result => res.json(result)) .catch(next); }, diff --git a/plugins/orchestrator-backend/src/types/pagination.ts b/plugins/orchestrator-backend/src/types/pagination.ts new file mode 100644 index 0000000000..6e37dbb7a3 --- /dev/null +++ b/plugins/orchestrator-backend/src/types/pagination.ts @@ -0,0 +1,32 @@ +import { Request } from 'express-serve-static-core'; + +import { + DEFAULT_PAGE_NUMBER, + DEFAULT_PAGE_SIZE, + DEFAULT_SORT_FIELD, + DEFAULT_SORT_ORDER, +} from '../service/constants'; + +export interface Pagination { + offset?: number; + limit?: number; + order?: string; + sortField?: string; +} + +export function buildPagination(req: Request): Pagination { + return { + offset: isNaN(req.query.page as any) + ? DEFAULT_PAGE_NUMBER + : Number(req.query.page), + limit: isNaN(req.query.pageSize as any) + ? DEFAULT_PAGE_SIZE + : Number(req.query.pageSize), + sortField: req.query.orderBy + ? String(req.query.orderBy) + : DEFAULT_SORT_FIELD, + order: req.query.orderDirection + ? String(req.query.orderDirection) + : DEFAULT_SORT_ORDER, + }; +} diff --git a/plugins/orchestrator-common/src/auto-generated/api/definition.ts b/plugins/orchestrator-common/src/auto-generated/api/definition.ts index 8b3b4debb3..7f6a2c930b 100644 --- a/plugins/orchestrator-common/src/auto-generated/api/definition.ts +++ b/plugins/orchestrator-common/src/auto-generated/api/definition.ts @@ -167,13 +167,47 @@ const OPENAPI = ` "operationId": "getInstances", "summary": "Get instances", "description": "Retrieve an array of instances", + "parameters": [ + { + "name": "page", + "in": "query", + "description": "page number", + "schema": { + "type": "number" + } + }, + { + "name": "pageSize", + "in": "query", + "description": "page size", + "schema": { + "type": "number" + } + }, + { + "name": "orderBy", + "in": "query", + "description": "field name to order the data", + "schema": { + "type": "string" + } + }, + { + "name": "orderDirection", + "in": "query", + "description": "ascending or descending", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProcessInstancesDTO" + "$ref": "#/components/schemas/ProcessInstanceListResultDTO" } } } @@ -463,11 +497,11 @@ const OPENAPI = ` "PaginationInfoDTO": { "type": "object", "properties": { - "limit": { + "pageSize": { "type": "number", "minimum": 0 }, - "offset": { + "page": { "type": "number", "minimum": 0 }, @@ -548,10 +582,18 @@ const OPENAPI = ` "format" ] }, - "ProcessInstancesDTO": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProcessInstanceDTO" + "ProcessInstanceListResultDTO": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessInstanceDTO" + } + }, + "paginationInfo": { + "$ref": "#/components/schemas/PaginationInfoDTO" + } } }, "AssessedProcessInstanceDTO": { diff --git a/plugins/orchestrator-common/src/auto-generated/api/models/schema.ts b/plugins/orchestrator-common/src/auto-generated/api/models/schema.ts index 89864045d3..f0d4e6f920 100644 --- a/plugins/orchestrator-common/src/auto-generated/api/models/schema.ts +++ b/plugins/orchestrator-common/src/auto-generated/api/models/schema.ts @@ -86,8 +86,8 @@ export interface components { description?: string; }; PaginationInfoDTO: { - limit?: number; - offset?: number; + pageSize?: number; + page?: number; totalCount?: number; }; /** @@ -115,7 +115,10 @@ export interface components { description?: string; annotations?: string[]; }; - ProcessInstancesDTO: components['schemas']['ProcessInstanceDTO'][]; + ProcessInstanceListResultDTO: { + items?: components['schemas']['ProcessInstanceDTO'][]; + paginationInfo?: components['schemas']['PaginationInfoDTO']; + }; AssessedProcessInstanceDTO: { instance: components['schemas']['ProcessInstanceDTO']; assessedBy?: components['schemas']['ProcessInstanceDTO']; @@ -303,11 +306,23 @@ export interface operations { * @description Retrieve an array of instances */ getInstances: { + parameters: { + query?: { + /** @description page number */ + page?: number; + /** @description page size */ + pageSize?: number; + /** @description field name to order the data */ + orderBy?: string; + /** @description ascending or descending */ + orderDirection?: string; + }; + }; responses: { /** @description Success */ 200: { content: { - 'application/json': components['schemas']['ProcessInstancesDTO']; + 'application/json': components['schemas']['ProcessInstanceListResultDTO']; }; }; /** @description Error fetching instances */ diff --git a/plugins/orchestrator-common/src/auto-generated/docs/index.adoc/index.adoc b/plugins/orchestrator-common/src/auto-generated/docs/index.adoc/index.adoc index 839750ac5c..59e6c7b336 100644 --- a/plugins/orchestrator-common/src/auto-generated/docs/index.adoc/index.adoc +++ b/plugins/orchestrator-common/src/auto-generated/docs/index.adoc/index.adoc @@ -326,11 +326,42 @@ Retrieve an array of instances +====== Query Parameters + +[cols="2,3,1,1,1"] +|=== +|Name| Description| Required| Default| Pattern + +| page +| page number +| - +| null +| + +| pageSize +| page size +| - +| null +| + +| orderBy +| field name to order the data +| - +| null +| + +| orderDirection +| ascending or descending +| - +| null +| + +|=== ===== Return Type -array[<>] +<> ===== Content Type @@ -347,7 +378,7 @@ array[<>] | 200 | Success -| List[<>] +| <> | 500 @@ -1039,13 +1070,13 @@ The ErrorResponse object represents a common structure for handling errors in AP |=== | Field Name| Required| Type| Description| Format -| limit +| pageSize | | BigDecimal | | -| offset +| page | | BigDecimal | @@ -1158,6 +1189,31 @@ The ErrorResponse object represents a common structure for handling errors in AP |=== +[#ProcessInstanceListResultDTO] +=== _ProcessInstanceListResultDTO_ + + + +[.fields-ProcessInstanceListResultDTO] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| items +| +| List of <> +| +| + +| paginationInfo +| +| PaginationInfoDTO +| +| + +|=== + + [#ProcessInstanceStatusDTO] === _ProcessInstanceStatusDTO_ diff --git a/plugins/orchestrator-common/src/openapi/openapi.yaml b/plugins/orchestrator-common/src/openapi/openapi.yaml index 9510ff67fb..43936bae00 100644 --- a/plugins/orchestrator-common/src/openapi/openapi.yaml +++ b/plugins/orchestrator-common/src/openapi/openapi.yaml @@ -103,13 +103,34 @@ paths: operationId: getInstances summary: Get instances description: Retrieve an array of instances + parameters: + - name: page + in: query + description: page number + schema: + type: number + - name: pageSize + in: query + description: page size + schema: + type: number + - name: orderBy + in: query + description: field name to order the data + schema: + type: string + - name: orderDirection + in: query + description: ascending or descending + schema: + type: string responses: '200': description: Success content: application/json: schema: - $ref: '#/components/schemas/ProcessInstancesDTO' + $ref: '#/components/schemas/ProcessInstanceListResultDTO' '500': description: Error fetching instances content: @@ -297,10 +318,10 @@ components: PaginationInfoDTO: type: object properties: - limit: + pageSize: type: number minimum: 0 - offset: + page: type: number minimum: 0 totalCount: @@ -357,10 +378,15 @@ components: - id - category - format - ProcessInstancesDTO: - type: array - items: - $ref: '#/components/schemas/ProcessInstanceDTO' + ProcessInstanceListResultDTO: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/ProcessInstanceDTO' + paginationInfo: + $ref: '#/components/schemas/PaginationInfoDTO' AssessedProcessInstanceDTO: type: object properties: diff --git a/plugins/orchestrator-common/src/openapi/types.ts b/plugins/orchestrator-common/src/openapi/types.ts index f7ac78e0d6..eb6038094a 100644 --- a/plugins/orchestrator-common/src/openapi/types.ts +++ b/plugins/orchestrator-common/src/openapi/types.ts @@ -8,7 +8,8 @@ export type WorkflowDTO = components['schemas']['WorkflowDTO']; export type WorkflowListResultDTO = components['schemas']['WorkflowListResultDTO']; export type ProcessInstanceDTO = components['schemas']['ProcessInstanceDTO']; -export type ProcessInstancesDTO = components['schemas']['ProcessInstancesDTO']; +export type ProcessInstanceListResultDTO = + components['schemas']['ProcessInstanceListResultDTO']; export type AssessedProcessInstanceDTO = components['schemas']['AssessedProcessInstanceDTO']; export type ExecuteWorkflowRequestDTO = From 743cfcaa764696587fc59150f8291cc1d1387123 Mon Sep 17 00:00:00 2001 From: Jude Niroshan Date: Wed, 6 Mar 2024 21:55:51 +0100 Subject: [PATCH 2/2] refactor: isolate pagination logics from /v1 endpoints --- .../src/service/DataIndexService.ts | 6 +++--- .../src/service/SonataFlowService.ts | 2 +- .../orchestrator-backend/src/service/api/v1.ts | 8 ++------ .../orchestrator-backend/src/service/api/v2.ts | 17 +++++++++-------- .../orchestrator-backend/src/service/router.ts | 11 ++++------- 5 files changed, 19 insertions(+), 25 deletions(-) diff --git a/plugins/orchestrator-backend/src/service/DataIndexService.ts b/plugins/orchestrator-backend/src/service/DataIndexService.ts index 80ef7a7a4d..bad1aa1eb1 100644 --- a/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -93,7 +93,7 @@ export class DataIndexService { } public async getWorkflowInfos( - pagination: Pagination, + pagination?: Pagination, ): Promise { this.logger.info(`getWorkflowInfos() called: ${this.dataIndexUrl}`); @@ -120,9 +120,9 @@ export class DataIndexService { } public async fetchProcessInstances( - pagination: Pagination, + pagination?: Pagination, ): Promise { - pagination.sortField ??= FETCH_PROCESS_INSTANCES_SORT_FIELD; + if (pagination) pagination.sortField ??= FETCH_PROCESS_INSTANCES_SORT_FIELD; const graphQlQuery = buildGraphQlQuery({ type: 'ProcessInstances', diff --git a/plugins/orchestrator-backend/src/service/SonataFlowService.ts b/plugins/orchestrator-backend/src/service/SonataFlowService.ts index e6c7498cd2..b72c49c9f7 100644 --- a/plugins/orchestrator-backend/src/service/SonataFlowService.ts +++ b/plugins/orchestrator-backend/src/service/SonataFlowService.ts @@ -145,7 +145,7 @@ export class SonataFlowService { } public async fetchWorkflowOverviews( - pagination: Pagination, + pagination?: Pagination, ): Promise { try { const workflowInfos = await this.dataIndex.getWorkflowInfos(pagination); diff --git a/plugins/orchestrator-backend/src/service/api/v1.ts b/plugins/orchestrator-backend/src/service/api/v1.ts index c06ea441ec..7540796ca8 100644 --- a/plugins/orchestrator-backend/src/service/api/v1.ts +++ b/plugins/orchestrator-backend/src/service/api/v1.ts @@ -10,7 +10,6 @@ import { WorkflowOverviewListResult, } from '@janus-idp/backstage-plugin-orchestrator-common'; -import { Pagination } from '../../types/pagination'; import { DataIndexService } from '../DataIndexService'; import { retryAsyncFunction } from '../Helper'; import { SonataFlowService } from '../SonataFlowService'; @@ -21,10 +20,8 @@ const FETCH_INSTANCE_RETRY_DELAY_MS = 1000; export namespace V1 { export async function getWorkflowsOverview( sonataFlowService: SonataFlowService, - pagination: Pagination, ): Promise { - const overviews = - await sonataFlowService.fetchWorkflowOverviews(pagination); + const overviews = await sonataFlowService.fetchWorkflowOverviews(); if (!overviews) { throw new Error("Couldn't fetch workflow overviews"); } @@ -79,9 +76,8 @@ export namespace V1 { export async function getInstances( dataIndexService: DataIndexService, - pagination: Pagination, ): Promise { - const instances = await dataIndexService.fetchProcessInstances(pagination); + const instances = await dataIndexService.fetchProcessInstances(); if (!instances) { throw new Error("Couldn't fetch process instances"); diff --git a/plugins/orchestrator-backend/src/service/api/v2.ts b/plugins/orchestrator-backend/src/service/api/v2.ts index 047bac153a..3caa042a88 100644 --- a/plugins/orchestrator-backend/src/service/api/v2.ts +++ b/plugins/orchestrator-backend/src/service/api/v2.ts @@ -31,16 +31,17 @@ export namespace V2 { sonataFlowService: SonataFlowService, pagination: Pagination, ): Promise { - const overviewsV1 = await V1.getWorkflowsOverview( - sonataFlowService, - pagination, - ); + const overviews = + await sonataFlowService.fetchWorkflowOverviews(pagination); + if (!overviews) { + throw new Error("Couldn't fetch workflow overviews"); + } const result: WorkflowOverviewListResultDTO = { - overviews: overviewsV1.items.map(item => mapToWorkflowOverviewDTO(item)), + overviews: overviews.map(item => mapToWorkflowOverviewDTO(item)), paginationInfo: { pageSize: pagination.limit, page: pagination.offset, - totalCount: overviewsV1.items?.length ?? 0, + totalCount: overviews.length, }, }; return result; @@ -87,11 +88,11 @@ export namespace V2 { dataIndexService: DataIndexService, pagination: Pagination, ): Promise { - const instances = await V1.getInstances(dataIndexService, pagination); + const instances = await dataIndexService.fetchProcessInstances(pagination); const totalCount = await dataIndexService.getProcessInstancesTotalCount(); const result: ProcessInstanceListResultDTO = { - items: instances.map(def => mapToProcessInstanceDTO(def)), + items: instances?.map(def => mapToProcessInstanceDTO(def)), paginationInfo: { pageSize: pagination.limit, page: pagination.offset, diff --git a/plugins/orchestrator-backend/src/service/router.ts b/plugins/orchestrator-backend/src/service/router.ts index f327210ecb..16b83d1bc1 100644 --- a/plugins/orchestrator-backend/src/service/router.ts +++ b/plugins/orchestrator-backend/src/service/router.ts @@ -145,11 +145,8 @@ function setupInternalRoutes( api: OpenAPIBackend, services: Services, ) { - router.get('/workflows/overview', async (req, res) => { - await V1.getWorkflowsOverview( - services.sonataFlowService, - buildPagination(req), - ) + router.get('/workflows/overview', async (_c, res) => { + await V1.getWorkflowsOverview(services.sonataFlowService) .then(result => res.status(200).json(result)) .catch(error => { res @@ -351,8 +348,8 @@ function setupInternalRoutes( }, ); - router.get('/instances', async (req, res) => { - await V1.getInstances(services.dataIndexService, buildPagination(req)) + router.get('/instances', async (_, res) => { + await V1.getInstances(services.dataIndexService) .then(result => res.status(200).json(result)) .catch(error => { res