From 7d8a88a98a1ada2aa29c3f148f8429dc61f1ef7f Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Mon, 23 Mar 2026 11:28:24 -0400 Subject: [PATCH 01/18] fix: Update grapql client to fix CVE-2026-3118. relates to JIRA https://redhat.atlassian.net/browse/RHIDP-12388 and https://redhat.atlassian.net/browse/RHIDP-12583 --- .../plugins/orchestrator-backend/package.json | 2 +- workspaces/orchestrator/yarn.lock | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/package.json b/workspaces/orchestrator/plugins/orchestrator-backend/package.json index cddaa5690cd..bcbe793e822 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/package.json +++ b/workspaces/orchestrator/plugins/orchestrator-backend/package.json @@ -76,7 +76,7 @@ "@backstage/plugin-scaffolder-node": "^0.12.4", "@red-hat-developer-hub/backstage-plugin-orchestrator-common": "workspace:^", "@red-hat-developer-hub/backstage-plugin-orchestrator-node": "workspace:^", - "@urql/core": "^4.1.4", + "@urql/core": "^6.0.1", "ajv-formats": "^2.1.1", "cloudevents": "^8.0.0", "express": "^4.21.2", diff --git a/workspaces/orchestrator/yarn.lock b/workspaces/orchestrator/yarn.lock index 38d5ed16670..a4da8a29fe7 100644 --- a/workspaces/orchestrator/yarn.lock +++ b/workspaces/orchestrator/yarn.lock @@ -5,15 +5,15 @@ __metadata: version: 8 cacheKey: 10c0 -"@0no-co/graphql.web@npm:^1.0.1": - version: 1.0.9 - resolution: "@0no-co/graphql.web@npm:1.0.9" +"@0no-co/graphql.web@npm:^1.0.13": + version: 1.2.0 + resolution: "@0no-co/graphql.web@npm:1.2.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: graphql: optional: true - checksum: 10c0/06c1acf62b0945d59f480481bc1a81b1542d5343bfef0e7cc88e4d582d49e242f3321f3a49b9e19f9d2cc270afa5415df7bed4f64ef1294b80c10f6d6b7b8602 + checksum: 10c0/4eed600962bfab42afb49cddcfb31a47b00502f59707609cf160559920ce0f5cf8874791e4cafc465ede30ae291992f3f892bc757b2a989e80e50e358f71c518 languageName: node linkType: hard @@ -12514,7 +12514,7 @@ __metadata: "@types/fs-extra": "npm:11.0.4" "@types/json-schema": "npm:7.0.15" "@types/luxon": "npm:^3.7.1" - "@urql/core": "npm:^4.1.4" + "@urql/core": "npm:^6.0.1" ajv-formats: "npm:^2.1.1" cloudevents: "npm:^8.0.0" express: "npm:^4.21.2" @@ -16575,13 +16575,13 @@ __metadata: languageName: node linkType: hard -"@urql/core@npm:^4.1.4": - version: 4.3.0 - resolution: "@urql/core@npm:4.3.0" +"@urql/core@npm:^6.0.1": + version: 6.0.1 + resolution: "@urql/core@npm:6.0.1" dependencies: - "@0no-co/graphql.web": "npm:^1.0.1" + "@0no-co/graphql.web": "npm:^1.0.13" wonka: "npm:^6.3.2" - checksum: 10c0/25a50cd11f27abca36ba07a93a393a3b0343d8d0957bf7fef4ddcc49d7582c751bb0c86f26c4f5e9342409237b92da569cfc90745a34539dfe8b5ebc426e112a + checksum: 10c0/44ff0d12dcef1e47338a9ff1217759d1124fa66eec1eec21ff9622e44c179b9d66fa78f462f195bfd8b790b04609abbe5a0674cbfcb0bc6d9c6fe6223d7d7b5b languageName: node linkType: hard From 7045d7a7a2b7c723e2301de06722a64909a5c5a6 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 24 Mar 2026 09:01:01 -0400 Subject: [PATCH 02/18] squash: add changeset --- workspaces/orchestrator/.changeset/five-meals-cover.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 workspaces/orchestrator/.changeset/five-meals-cover.md diff --git a/workspaces/orchestrator/.changeset/five-meals-cover.md b/workspaces/orchestrator/.changeset/five-meals-cover.md new file mode 100644 index 00000000000..0e602ff9210 --- /dev/null +++ b/workspaces/orchestrator/.changeset/five-meals-cover.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator-backend': patch +--- + +Update dependecy @urql/core to fix CVE-2026-3118 From 7ce9aadf30be0c84058468a800929e514a832afe Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 31 Mar 2026 13:11:41 -0400 Subject: [PATCH 03/18] squash: pagination now using query variables. Next up is the filter logic. ugh --- .../src/helpers/queryBuilder.test.ts | 15 ++-- .../src/helpers/queryBuilder.ts | 58 ++++++++------ .../src/service/DataIndexService.test.ts | 80 ++++++++++++++++--- .../src/service/DataIndexService.ts | 37 ++++++++- .../src/types/pagination.ts | 8 +- 5 files changed, 147 insertions(+), 51 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts index ab77d65abba..2638644501c 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Pagination } from '../types/pagination'; import { buildGraphQlQuery } from './queryBuilder'; @@ -57,7 +58,7 @@ describe('buildGraphQlQuery', () => { whereClause: '', pagination: {}, }, - expectedResult: `{${defaultTestParams.type} {${defaultTestParams.queryBody} } }`, + expectedResult: `query ($paginationInfo: Pagination, $orderByInfo: ${defaultTestParams.type.slice(0, -1)}OrderBy){${defaultTestParams.type} (orderBy: $orderByInfo, pagination: $paginationInfo) {${defaultTestParams.queryBody} } }`, }, { name: 'should build a query with a where clause', @@ -67,7 +68,7 @@ describe('buildGraphQlQuery', () => { whereClause: defaultTestParams.whereClause, pagination: {}, }, - expectedResult: `{${defaultTestParams.type} (where: {${defaultTestParams.whereClause}}) {${defaultTestParams.queryBody} } }`, + expectedResult: `query ($paginationInfo: Pagination, $orderByInfo: ${defaultTestParams.type.slice(0, -1)}OrderBy){${defaultTestParams.type} (where: {${defaultTestParams.whereClause}}, orderBy: $orderByInfo, pagination: $paginationInfo) {${defaultTestParams.queryBody} } }`, }, { name: 'should build a query with pagination', @@ -77,18 +78,16 @@ describe('buildGraphQlQuery', () => { whereClause: '', pagination: defaultTestParams.pagination, }, - expectedResult: `{${defaultTestParams.type} (${getPaginationString( - defaultTestParams.pagination, - )} {${defaultTestParams.queryBody} } }`, + expectedResult: `query ($paginationInfo: Pagination, $orderByInfo: ${defaultTestParams.type.slice(0, -1)}OrderBy){${defaultTestParams.type} (orderBy: $orderByInfo, pagination: $paginationInfo) {${defaultTestParams.queryBody} } }`, }, { name: 'should build a query with both where clause and pagination', params: { ...defaultTestParams, }, - expectedResult: `{${defaultTestParams.type} (where: {${ + expectedResult: `query ($paginationInfo: Pagination, $orderByInfo: ${defaultTestParams.type.slice(0, -1)}OrderBy){${defaultTestParams.type} (where: {${ defaultTestParams.whereClause - }}, ${getPaginationString(defaultTestParams.pagination)} {${ + }}, orderBy: $orderByInfo, pagination: $paginationInfo) {${ defaultTestParams.queryBody } } }`, }, diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts index e33d20db3f5..ffd47dd017c 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Pagination } from '../types/pagination'; + +import { Pagination, PaginationQueryVariable } from '../types/pagination'; export function buildGraphQlQuery(args: { type: 'ProcessDefinitions' | 'ProcessInstances' | 'Jobs'; @@ -21,14 +22,18 @@ export function buildGraphQlQuery(args: { whereClause?: string; pagination?: Pagination; }): string { - let query = `{${args.type}`; + // TODO: add variables to the top, https://graphql.org/learn/queries/#variables + let query = `query ($paginationInfo: Pagination, $orderByInfo: ${args.type.slice(0, -1)}OrderBy){${args.type}`; const whereClause = buildWhereClause(args.whereClause); - const paginationClause = buildPaginationClause(args.pagination); + const paginationClause = 'pagination: $paginationInfo'; + const orderByClause = 'orderBy: $orderByInfo'; - if (whereClause || paginationClause) { + if (whereClause || paginationClause || orderByClause) { query += ' ('; - query += [whereClause, paginationClause].filter(Boolean).join(', '); + query += [whereClause, orderByClause, paginationClause] + .filter(Boolean) + .join(', '); query += ') '; } @@ -41,29 +46,30 @@ function buildWhereClause(whereClause?: string): string { return whereClause ? `where: {${whereClause}}` : ''; } -function buildPaginationClause(pagination?: Pagination): string { - if (!pagination) return ''; - - const parts = []; +export function buildOrderByVariables(pagination?: Pagination): { + [key: string]: string; +} { + const orderByVariable: { [key: string]: string } = {}; - if (pagination.sortField !== undefined) { - parts.push( - `orderBy: {${pagination.sortField}: ${ - pagination.order !== undefined ? pagination.order?.toUpperCase() : 'ASC' - }}`, - ); + if (pagination?.sortField !== undefined) { + orderByVariable[pagination.sortField] = + pagination.order !== undefined ? pagination.order?.toUpperCase() : 'ASC'; } - const paginationParts = []; - if (pagination.limit !== undefined) { - paginationParts.push(`limit: ${pagination.limit}`); - } - if (pagination.offset !== undefined) { - paginationParts.push(`offset: ${pagination.offset}`); - } - if (paginationParts.length) { - parts.push(`pagination: {${paginationParts.join(', ')}}`); + return orderByVariable; +} + +export function buildPaginationVariables( + pagination?: Pagination, +): PaginationQueryVariable { + const paginationVariable: PaginationQueryVariable = {}; + + if (pagination?.limit !== undefined) { + paginationVariable.limit = pagination.limit; } - return parts.join(', '); + if (pagination?.offset !== undefined) { + paginationVariable.offset = pagination.offset; + } + return paginationVariable; } diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts index c5ec7ff4903..05439c5a6f3 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts @@ -252,7 +252,10 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); }); @@ -289,7 +292,10 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); }); @@ -327,7 +333,15 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalledTimes(1); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: { + name: 'ASC', + }, + paginationInfo: { + limit: 10, + offset: 0, + }, + }, ); expect(buildFilterConditionSpy).not.toHaveBeenCalled(); }); @@ -373,7 +387,10 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); }); @@ -422,7 +439,10 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessDefinitions); @@ -461,7 +481,15 @@ describe('fetchWorkflowInfos', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: { + name: 'ASC', + }, + paginationInfo: { + limit: 10, + offset: 0, + }, + }, ); expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(2); expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ @@ -598,7 +626,10 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); }); @@ -632,7 +663,10 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessInstances); @@ -673,7 +707,15 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalledTimes(1); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: { + name: 'ASC', + }, + paginationInfo: { + limit: 10, + offset: 0, + }, + }, ); }); @@ -712,7 +754,10 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); }); @@ -749,7 +794,10 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: {}, + paginationInfo: {}, + }, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessInstances); @@ -791,7 +839,15 @@ describe('fetchInstances', () => { expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - {}, + { + orderByInfo: { + name: 'ASC', + }, + paginationInfo: { + limit: 10, + offset: 0, + }, + }, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessInstances); diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index 22231c52a54..563f91613b3 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -31,7 +31,11 @@ import { import { ErrorBuilder } from '../helpers/errorBuilder'; import { buildFilterCondition } from '../helpers/filterBuilder'; -import { buildGraphQlQuery } from '../helpers/queryBuilder'; +import { + buildGraphQlQuery, + buildOrderByVariables, + buildPaginationVariables, +} from '../helpers/queryBuilder'; import { Pagination } from '../types/pagination'; import { FETCH_PROCESS_INSTANCES_SORT_FIELD } from './constants'; @@ -215,6 +219,10 @@ export class DataIndexService { ? `id: {in: ${JSON.stringify(definitionIds)}}` : undefined; + // TODO: something with filters + // We need to know how many filters there are so we can create those variables + // Then somehow add those to the top of the query + // Then add those to our variable list when exectuting the client const filterCondition = filter ? buildFilterCondition( await this.initInputProcessDefinitionArgs(), @@ -223,6 +231,8 @@ export class DataIndexService { ) : undefined; + // console.log(`Filter Condition: ${filterCondition}`); + let whereClause: string | undefined; if (definitionIdsCondition && filterCondition) { whereClause = `and: [{${definitionIdsCondition}}, {${filterCondition}}]`; @@ -232,6 +242,8 @@ export class DataIndexService { whereClause = undefined; } + // console.log(`Where Clause: ${whereClause}`); + const graphQlQuery = buildGraphQlQuery({ type: 'ProcessDefinitions', queryBody: @@ -240,7 +252,11 @@ export class DataIndexService { pagination, }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); - const result = await this.client.query(graphQlQuery, {}); + // console.log(`GraphQL query: ${graphQlQuery}`); + const result = await this.client.query(graphQlQuery, { + paginationInfo: buildPaginationVariables(pagination), + orderByInfo: buildOrderByVariables(pagination), + }); this.logger.debug( `Get workflow definitions result: ${JSON.stringify(result)}`, ); @@ -275,6 +291,8 @@ export class DataIndexService { ) : ''; + // console.log(`Filter Condition: ${filterCondition}`); + let whereClause = ''; const conditions = []; @@ -298,6 +316,8 @@ export class DataIndexService { whereClause = `and: [${conditions.join(', ')}]`; } + // console.log(`Where Clause: ${whereClause}`); + const graphQlQuery = buildGraphQlQuery({ type: 'ProcessInstances', queryBody: @@ -307,10 +327,14 @@ export class DataIndexService { }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); + // console.log(`GraphQL query: ${graphQlQuery}`); const result = await this.client.query<{ ProcessInstances: ProcessInstance[]; - }>(graphQlQuery, {}); + }>(graphQlQuery, { + paginationInfo: buildPaginationVariables(pagination), + orderByInfo: buildOrderByVariables(pagination), + }); this.logger.debug( `Fetch process instances result: ${JSON.stringify(result)}`, ); @@ -352,6 +376,8 @@ export class DataIndexService { const whereClause = `and: [{${processIdNotNullCondition}}, {${filterCondition}}]`; + // console.log(`Where Clause: ${whereClause}`); + // Apply a limit to prevent memory exhaustion and network timeouts when entities // have thousands of process instances. Entities with more instances than this limit // may not see all their associated workflows. @@ -371,7 +397,10 @@ export class DataIndexService { const result = await this.client.query<{ ProcessInstances: ProcessInstance[]; - }>(graphQlQuery, {}); + }>(graphQlQuery, { + paginationInfo: buildPaginationVariables(pagination), + orderByInfo: buildOrderByVariables(pagination), + }); this.logger.debug( `Fetch definition ids from instances history result: ${JSON.stringify(result)}`, ); diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/types/pagination.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/pagination.ts index b60ab673cf0..f77358bd05f 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/types/pagination.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/pagination.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Request } from 'express-serve-static-core'; import { PaginationInfoDTO } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; @@ -24,6 +25,11 @@ export interface Pagination { sortField?: string; } +export interface PaginationQueryVariable { + offset?: number; + limit?: number; +} + export function buildPagination(req: Request): Pagination { const pagination: Pagination = { limit: undefined, From c78bf3652fc75290c06248844e9e70991396010d Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 31 Mar 2026 13:17:10 -0400 Subject: [PATCH 04/18] squash: fix tsc errors --- .../src/helpers/queryBuilder.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts index 2638644501c..c36727601bb 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.test.ts @@ -33,16 +33,6 @@ describe('buildGraphQlQuery', () => { whereClause: 'version: "1.0"', }; - const getPaginationString = (pagination: Pagination | undefined) => { - const paginationOrder = pagination?.order - ? pagination.order.toUpperCase() - : 'ASC'; - if (pagination) { - return `orderBy: {${pagination.sortField}: ${paginationOrder}}, pagination: {limit: ${pagination.limit}, offset: ${pagination.offset}})`; - } - return undefined; - }; - type TestCase = { name: string; params: typeof defaultTestParams; From dcdadcc1572a2392a06386ad68acac0a5398b1c0 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 31 Mar 2026 16:00:27 -0400 Subject: [PATCH 05/18] squash: just saving this off for now, go get this later --- .../src/helpers/filterBuilder.ts | 52 ++- .../src/helpers/filterBuilders.test.ts | 358 +++++++++--------- .../src/helpers/queryBuilder.ts | 4 +- .../src/service/DataIndexService.ts | 12 +- 4 files changed, 234 insertions(+), 192 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index f07fc777175..5c768823d5b 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -136,7 +136,8 @@ function handleBinaryOperator( binaryFilter: FieldFilter, fieldDef: IntrospectionField | undefined, type: 'ProcessDefinition' | 'ProcessInstance', -): string { +): any { + // TODO: Needs to change if (isEnumFilter(binaryFilter.field, type)) { if (!isValidEnumOperator(binaryFilter.operator)) { throw new Error( @@ -149,9 +150,31 @@ function handleBinaryOperator( .map(v => formatValue(binaryFilter.field, v, fieldDef, type)) .join(', ')}]` : formatValue(binaryFilter.field, binaryFilter.value, fieldDef, type); - return `${binaryFilter.field}: {${getGraphQLOperator( - binaryFilter.operator, - )}: ${formattedValue}}`; + // Return some object that has the string value but instead of the formatted value, replace with the variable + // the object also has the variableName and the formatted value as properties + /** + * { + * clause: `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator,)}: $someVariableName}` + * formattedValue: 'some string', + * variableName: '$someVariableName' + * } + */ + const nonSecureRandomAlphaNumeric = Math.random() + .toString(36) + .slice(2) + .slice(0, 4); + const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric}`; + const clause = `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator)}: $${clauseVariableName}}`; + const clauseObject = { + formattedValue: formattedValue, + clauseVariableName: clauseVariableName, + clause, + }; + console.log(clauseObject); + return clauseObject; + // return `${binaryFilter.field}: {${getGraphQLOperator( + // binaryFilter.operator, + // )}: ${formattedValue}}`; } export function buildFilterCondition( @@ -159,7 +182,8 @@ export function buildFilterCondition( type: ProcessType, filters?: Filter, isNested?: boolean, -): string { +): any { + // TODO: This needs to return an object instead of the string if (!filters) { return ''; } @@ -195,11 +219,15 @@ export function buildFilterCondition( } } + let handledFilterOperation; + switch (filters.operator) { case FieldFilterOperatorEnum.IsNull: - return handleIsNullOperator(filters); + handledFilterOperation = handleIsNullOperator(filters); + break; case FieldFilterOperatorEnum.Between: - return handleBetweenOperator(filters); + handledFilterOperation = handleBetweenOperator(filters); + break; case FieldFilterOperatorEnum.Eq: case FieldFilterOperatorEnum.Like: case FieldFilterOperatorEnum.In: @@ -207,11 +235,15 @@ export function buildFilterCondition( case FieldFilterOperatorEnum.Gte: case FieldFilterOperatorEnum.Lt: case FieldFilterOperatorEnum.Lte: - return handleBinaryOperator(filters, fieldDef, type); + handledFilterOperation = handleBinaryOperator(filters, fieldDef, type); + break; default: throw new Error(`Can't build filter condition`); } + + console.log(handledFilterOperation); + return handledFilterOperation; } function isOperatorSupported(operator: FieldFilterOperatorEnum): boolean { @@ -255,7 +287,7 @@ function formatValue( type: ProcessType, ): string { if (!fieldDef) { - return `"${fieldValue}"`; + return `${fieldValue}`; } if (!isFieldFilterSupported) { @@ -270,7 +302,7 @@ function formatValue( fieldDef.type.name === TypeName.Id || fieldDef.type.name === TypeName.Date ) { - return `"${fieldValue}"`; + return `${fieldValue}`; } throw new Error( `Failed to format value for ${fieldName} ${fieldValue} with type ${fieldDef.type.name}`, diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts index b4e92792287..23023b25cc9 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts @@ -29,7 +29,7 @@ import { isOperatorAllowedForField, } from './filterBuilder'; -describe('isOperatorAllowedForField', () => { +describe.skip('isOperatorAllowedForField', () => { const testIsValidOperator = ( operator: FieldFilterOperatorEnum, fieldDef: IntrospectionField, @@ -128,7 +128,7 @@ describe('column filters', () => { filter: Filter | undefined; expectedResult: string; }; - describe('empty filter testcases', () => { + describe.skip('empty filter testcases', () => { const emptyFilterTestCases: FilterTestCase[] = [ { name: 'returns empty string when filters are null or undefined', @@ -164,180 +164,180 @@ describe('column filters', () => { ), expectedResult: 'name: {equal: "Hello World Workflow"}', }, - { - name: 'returns correct filter for single string field with like operator', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter( - 'name', - FieldFilterOperatorEnum.Like, - 'Hello%', - ), - expectedResult: 'name: {like: "Hello%"}', - }, - { - name: 'returns correct filter for string field with isNull operator (true)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter('name', FieldFilterOperatorEnum.IsNull, true), - expectedResult: 'name: {isNull: true}', - }, - { - name: 'returns correct filter for string field with isNull operator (false)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter( - 'name', - FieldFilterOperatorEnum.IsNull, - false, - ), - expectedResult: 'name: {isNull: false}', - }, - { - name: 'returns correct filter for string field with isNull operator ("true" as string)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter( - 'name', - FieldFilterOperatorEnum.IsNull, - 'True', - ), - expectedResult: 'name: {isNull: true}', - }, - { - name: 'returns correct filter for string field with isNull operator ("false" as string)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter( - 'name', - FieldFilterOperatorEnum.IsNull, - 'FALSE', - ), - expectedResult: 'name: {isNull: false}', - }, - { - name: 'returns correct filter for string field with in operator (single value)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ - 'Test String', - ]), - expectedResult: 'name: {in: ["Test String"]}', - }, - { - name: 'returns correct filter for string field with in operator (multiple values)', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ - 'Test String 1', - 'Test String 2', - 'Test String 3', - ]), - expectedResult: - 'name: {in: ["Test String 1", "Test String 2", "Test String 3"]}', - }, - { - name: 'returns correct OR filter for two string fields with equal operator', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - createIntrospectionField('processName', TypeName.String), - ], - filter: { - operator: 'OR', - filters: [ - createFieldFilter( - 'name', - FieldFilterOperatorEnum.Eq, - 'Hello World Workflow', - ), - createFieldFilter( - 'processName', - FieldFilterOperatorEnum.Eq, - 'Greeting workflow', - ), - ], - }, - expectedResult: - 'or: {name: {equal: "Hello World Workflow"}, processName: {equal: "Greeting workflow"}}', - }, - { - name: 'returns correct filter for string field with like and isNull operators', - introspectionFields: [ - createIntrospectionField('description', TypeName.String), - ], - filter: { - operator: 'OR', - filters: [ - createFieldFilter( - 'description', - FieldFilterOperatorEnum.Like, - '%Test%', - ), - createFieldFilter( - 'description', - FieldFilterOperatorEnum.IsNull, - true, - ), - ], - }, - expectedResult: - 'or: {description: {like: "%Test%"}, description: {isNull: true}}', - }, - { - name: 'returns correct filter for string field with in, like, equal, and isNull operators', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: { - operator: 'OR', - filters: [ - createFieldFilter('name', FieldFilterOperatorEnum.In, [ - 'Test String 1', - 'Test String 2', - ]), - createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), - createFieldFilter( - 'name', - FieldFilterOperatorEnum.Eq, - 'Exact Match', - ), - createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), - ], - }, - expectedResult: - 'or: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', - }, - { - name: 'returns correct filter for string field with in, like, equal, and isNull operators', - introspectionFields: [ - createIntrospectionField('name', TypeName.String), - ], - filter: { - operator: 'AND', - filters: [ - createFieldFilter('name', FieldFilterOperatorEnum.In, [ - 'Test String 1', - 'Test String 2', - ]), - createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), - createFieldFilter( - 'name', - FieldFilterOperatorEnum.Eq, - 'Exact Match', - ), - createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), - ], - }, - expectedResult: - 'and: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', - }, + // { + // name: 'returns correct filter for single string field with like operator', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.Like, + // 'Hello%', + // ), + // expectedResult: 'name: {like: "Hello%"}', + // }, + // { + // name: 'returns correct filter for string field with isNull operator (true)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter('name', FieldFilterOperatorEnum.IsNull, true), + // expectedResult: 'name: {isNull: true}', + // }, + // { + // name: 'returns correct filter for string field with isNull operator (false)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.IsNull, + // false, + // ), + // expectedResult: 'name: {isNull: false}', + // }, + // { + // name: 'returns correct filter for string field with isNull operator ("true" as string)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.IsNull, + // 'True', + // ), + // expectedResult: 'name: {isNull: true}', + // }, + // { + // name: 'returns correct filter for string field with isNull operator ("false" as string)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.IsNull, + // 'FALSE', + // ), + // expectedResult: 'name: {isNull: false}', + // }, + // { + // name: 'returns correct filter for string field with in operator (single value)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ + // 'Test String', + // ]), + // expectedResult: 'name: {in: ["Test String"]}', + // }, + // { + // name: 'returns correct filter for string field with in operator (multiple values)', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ + // 'Test String 1', + // 'Test String 2', + // 'Test String 3', + // ]), + // expectedResult: + // 'name: {in: ["Test String 1", "Test String 2", "Test String 3"]}', + // }, + // { + // name: 'returns correct OR filter for two string fields with equal operator', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // createIntrospectionField('processName', TypeName.String), + // ], + // filter: { + // operator: 'OR', + // filters: [ + // createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.Eq, + // 'Hello World Workflow', + // ), + // createFieldFilter( + // 'processName', + // FieldFilterOperatorEnum.Eq, + // 'Greeting workflow', + // ), + // ], + // }, + // expectedResult: + // 'or: {name: {equal: "Hello World Workflow"}, processName: {equal: "Greeting workflow"}}', + // }, + // { + // name: 'returns correct filter for string field with like and isNull operators', + // introspectionFields: [ + // createIntrospectionField('description', TypeName.String), + // ], + // filter: { + // operator: 'OR', + // filters: [ + // createFieldFilter( + // 'description', + // FieldFilterOperatorEnum.Like, + // '%Test%', + // ), + // createFieldFilter( + // 'description', + // FieldFilterOperatorEnum.IsNull, + // true, + // ), + // ], + // }, + // expectedResult: + // 'or: {description: {like: "%Test%"}, description: {isNull: true}}', + // }, + // { + // name: 'returns correct filter for string field with in, like, equal, and isNull operators', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: { + // operator: 'OR', + // filters: [ + // createFieldFilter('name', FieldFilterOperatorEnum.In, [ + // 'Test String 1', + // 'Test String 2', + // ]), + // createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), + // createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.Eq, + // 'Exact Match', + // ), + // createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), + // ], + // }, + // expectedResult: + // 'or: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', + // }, + // { + // name: 'returns correct filter for string field with in, like, equal, and isNull operators', + // introspectionFields: [ + // createIntrospectionField('name', TypeName.String), + // ], + // filter: { + // operator: 'AND', + // filters: [ + // createFieldFilter('name', FieldFilterOperatorEnum.In, [ + // 'Test String 1', + // 'Test String 2', + // ]), + // createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), + // createFieldFilter( + // 'name', + // FieldFilterOperatorEnum.Eq, + // 'Exact Match', + // ), + // createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), + // ], + // }, + // expectedResult: + // 'and: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', + // }, ]; stringTestCases.forEach( ({ name, introspectionFields, filter, expectedResult }) => { @@ -352,7 +352,7 @@ describe('column filters', () => { }, ); }); - describe('idArgument testcases', () => { + describe.skip('idArgument testcases', () => { const idTestCases: FilterTestCase[] = [ { name: 'returns correct filter for single id field with equal operator', @@ -443,7 +443,7 @@ describe('column filters', () => { }, ); }); - describe('dateArgument testcases', () => { + describe.skip('dateArgument testcases', () => { const testDate1 = '2024-10-10T09:54:40.759Z'; const testDate2 = '2025-10-10T09:54:40.759Z'; @@ -610,7 +610,7 @@ describe('column filters', () => { }, ); }); - describe('enumArgument testcases', () => { + describe.skip('enumArgument testcases', () => { const idTestCases: FilterTestCase[] = [ { name: 'returns correct filter for state enum field with equal operator', diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts index ffd47dd017c..c40d07b806a 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts @@ -15,15 +15,17 @@ */ import { Pagination, PaginationQueryVariable } from '../types/pagination'; +import { buildFilterCondition } from './filterBuilder'; export function buildGraphQlQuery(args: { type: 'ProcessDefinitions' | 'ProcessInstances' | 'Jobs'; queryBody: string; whereClause?: string; pagination?: Pagination; + filterCondition?: any; }): string { // TODO: add variables to the top, https://graphql.org/learn/queries/#variables - let query = `query ($paginationInfo: Pagination, $orderByInfo: ${args.type.slice(0, -1)}OrderBy){${args.type}`; + let query = `query ($paginationInfo: Pagination, $orderByInfo: ${args.type.slice(0, -1)}OrderBy, $${args.filterCondition.clauseVariableName}: String){${args.type}`; const whereClause = buildWhereClause(args.whereClause); const paginationClause = 'pagination: $paginationInfo'; diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index 563f91613b3..a1f61107fa4 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -223,6 +223,11 @@ export class DataIndexService { // We need to know how many filters there are so we can create those variables // Then somehow add those to the top of the query // Then add those to our variable list when exectuting the client + // TODO: the buildFilterCondition function needs to return an object that has the following: + // A list of the filters + // The fileters object should also include what the variable name will be and the value associated + // That list should be passed into the query builder function so then can get populated on the "header thing" + // That list will also get spread during the call to the query const filterCondition = filter ? buildFilterCondition( await this.initInputProcessDefinitionArgs(), @@ -231,11 +236,11 @@ export class DataIndexService { ) : undefined; - // console.log(`Filter Condition: ${filterCondition}`); + console.log(`Filter Condition: ${filterCondition}`); let whereClause: string | undefined; if (definitionIdsCondition && filterCondition) { - whereClause = `and: [{${definitionIdsCondition}}, {${filterCondition}}]`; + whereClause = `and: [{${definitionIdsCondition}}, {${filterCondition.clause}}]`; } else if (definitionIdsCondition || filterCondition) { whereClause = definitionIdsCondition ?? filterCondition; } else { @@ -250,12 +255,15 @@ export class DataIndexService { 'id, name, version, type, endpoint, serviceUrl, source, metadata', whereClause, pagination, + filterCondition: filterCondition, }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); // console.log(`GraphQL query: ${graphQlQuery}`); + // Spread the values here const result = await this.client.query(graphQlQuery, { paginationInfo: buildPaginationVariables(pagination), orderByInfo: buildOrderByVariables(pagination), + [filterCondition.clauseVariableName]: filterCondition.formattedValue, }); this.logger.debug( `Get workflow definitions result: ${JSON.stringify(result)}`, From 727d7300748db9e51ea18004810a5b63a75ee0e5 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Fri, 3 Apr 2026 11:00:21 -0400 Subject: [PATCH 06/18] squash: almost there, need to fix some tests --- .../src/helpers/filterBuilder.ts | 152 +++-- .../src/helpers/filterBuilders.test.ts | 537 +++++++++++------- .../src/helpers/queryBuilder.ts | 38 +- .../src/service/DataIndexService.ts | 71 +-- .../src/types/filterClause.ts | 26 + 5 files changed, 512 insertions(+), 312 deletions(-) create mode 100644 workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index 5c768823d5b..6d6a7de5023 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -24,6 +24,8 @@ import { TypeName, } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; +import { FilterClause, FilterClauseVariable } from '../types/filterClause'; + type ProcessType = 'ProcessDefinition' | 'ProcessInstance'; const supportedOperators = [ @@ -73,21 +75,26 @@ function handleLogicalFilter( introspection: IntrospectionField[], type: ProcessType, filter: LogicalFilter, -): string { - if (!filter.operator) return ''; +): FilterClause { + if (!filter.operator) return {} as FilterClause; const subClauses = filter.filters.map(f => buildFilterCondition(introspection, type, f), ); - return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`; + const filterClause: FilterClause = { + clause: `${filter.operator.toLowerCase()}: {${subClauses.map(cl => cl.clause).join(', ')}}`, + clauseVariable: subClauses.flatMap(cl => cl.clauseVariable), + }; + return filterClause; + // return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`; } function handleNestedFilter( introspection: IntrospectionField[], type: ProcessType, filter: NestedFilter, -): string { +): FilterClause { const subClauses = buildFilterCondition( introspection, type, @@ -95,22 +102,63 @@ function handleNestedFilter( true, ); - return `${filter.field}: {${subClauses}}`; + const filterClause: FilterClause = { + clauseVariable: subClauses.clauseVariable, + clause: `${filter.field}: {${subClauses.clause}}`, + }; + + return filterClause; } -function handleBetweenOperator(filter: FieldFilter): string { +function handleBetweenOperator(filter: FieldFilter): FilterClause { if (!Array.isArray(filter.value) || filter.value.length !== 2) { throw new Error('Between operator requires an array of two elements'); } - return `${filter.field}: {${getGraphQLOperator( + const filterClauseVariableArray: FilterClauseVariable[] = []; + const clauseVariableName1 = `clauseVariable${nonSecureRandomAlphaNumeric()}`; + const filterClauseVariable1: FilterClauseVariable = { + clauseVariableName: clauseVariableName1, + formattedValue: filter.value[0], + clauseVariableType: 'String', + }; + + const clauseVariableName2 = `clauseVariable${nonSecureRandomAlphaNumeric()}`; + const filterClauseVariable2: FilterClauseVariable = { + clauseVariableName: clauseVariableName2, + formattedValue: filter.value[1], + clauseVariableType: 'String', + }; + + const clause = `${filter.field}: {${getGraphQLOperator( FieldFilterOperatorEnum.Between, - )}: {from: "${filter.value[0]}", to: "${filter.value[1]}"}}`; + )}: {from: $${clauseVariableName1}, to: $${clauseVariableName2}}}`; + filterClauseVariableArray.push(filterClauseVariable1); + filterClauseVariableArray.push(filterClauseVariable2); + const filterClause: FilterClause = { + clause: clause, + clauseVariable: filterClauseVariableArray, + }; + + return filterClause; } -function handleIsNullOperator(filter: FieldFilter): string { - return `${filter.field}: {${getGraphQLOperator( - FieldFilterOperatorEnum.IsNull, - )}: ${convertToBoolean(filter.value)}}`; +function handleIsNullOperator(filter: FieldFilter): FilterClause { + const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`; + const clause = `${filter.field}: {${getGraphQLOperator(FieldFilterOperatorEnum.IsNull)}: $${clauseVariableName}}`; + + const filterClauseVariable: FilterClauseVariable = { + clauseVariableName: clauseVariableName, + formattedValue: convertToBoolean(filter.value), + clauseVariableType: 'Boolean', + }; + const filterClauseVariableArray: FilterClauseVariable[] = []; + filterClauseVariableArray.push(filterClauseVariable); + const clauseObject: FilterClause = { + clauseVariable: filterClauseVariableArray, + clause, + }; + + return clauseObject; } function isEnumFilter( @@ -136,8 +184,7 @@ function handleBinaryOperator( binaryFilter: FieldFilter, fieldDef: IntrospectionField | undefined, type: 'ProcessDefinition' | 'ProcessInstance', -): any { - // TODO: Needs to change +): FilterClause { if (isEnumFilter(binaryFilter.field, type)) { if (!isValidEnumOperator(binaryFilter.operator)) { throw new Error( @@ -145,36 +192,38 @@ function handleBinaryOperator( ); } } - const formattedValue = Array.isArray(binaryFilter.value) - ? `[${binaryFilter.value - .map(v => formatValue(binaryFilter.field, v, fieldDef, type)) - .join(', ')}]` - : formatValue(binaryFilter.field, binaryFilter.value, fieldDef, type); - // Return some object that has the string value but instead of the formatted value, replace with the variable - // the object also has the variableName and the formatted value as properties - /** - * { - * clause: `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator,)}: $someVariableName}` - * formattedValue: 'some string', - * variableName: '$someVariableName' - * } - */ - const nonSecureRandomAlphaNumeric = Math.random() - .toString(36) - .slice(2) - .slice(0, 4); - const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric}`; + let formattedValue: any; + let paramType: string; + if (Array.isArray(binaryFilter.value)) { + formattedValue = binaryFilter.value.map(v => + formatValue(binaryFilter.field, v, fieldDef, type), + ); + paramType = '[String!]'; + } else { + formattedValue = formatValue( + binaryFilter.field, + binaryFilter.value, + fieldDef, + type, + ); + paramType = 'String'; + } + + const clauseVariableName = `clauseVariable${nonSecureRandomAlphaNumeric()}`; const clause = `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator)}: $${clauseVariableName}}`; - const clauseObject = { - formattedValue: formattedValue, + const filterClauseVariable: FilterClauseVariable = { clauseVariableName: clauseVariableName, + formattedValue: formattedValue, + clauseVariableType: paramType, + }; + const filterClauseVariableArray: FilterClauseVariable[] = []; + filterClauseVariableArray.push(filterClauseVariable); + const clauseObject: FilterClause = { + clauseVariable: filterClauseVariableArray, clause, }; - console.log(clauseObject); + return clauseObject; - // return `${binaryFilter.field}: {${getGraphQLOperator( - // binaryFilter.operator, - // )}: ${formattedValue}}`; } export function buildFilterCondition( @@ -182,10 +231,9 @@ export function buildFilterCondition( type: ProcessType, filters?: Filter, isNested?: boolean, -): any { - // TODO: This needs to return an object instead of the string +): FilterClause { if (!filters) { - return ''; + return {} as FilterClause; } if (isNestedFilter(filters)) { @@ -219,15 +267,11 @@ export function buildFilterCondition( } } - let handledFilterOperation; - switch (filters.operator) { case FieldFilterOperatorEnum.IsNull: - handledFilterOperation = handleIsNullOperator(filters); - break; + return handleIsNullOperator(filters); case FieldFilterOperatorEnum.Between: - handledFilterOperation = handleBetweenOperator(filters); - break; + return handleBetweenOperator(filters); case FieldFilterOperatorEnum.Eq: case FieldFilterOperatorEnum.Like: case FieldFilterOperatorEnum.In: @@ -235,15 +279,11 @@ export function buildFilterCondition( case FieldFilterOperatorEnum.Gte: case FieldFilterOperatorEnum.Lt: case FieldFilterOperatorEnum.Lte: - handledFilterOperation = handleBinaryOperator(filters, fieldDef, type); - break; + return handleBinaryOperator(filters, fieldDef, type); default: throw new Error(`Can't build filter condition`); } - - console.log(handledFilterOperation); - return handledFilterOperation; } function isOperatorSupported(operator: FieldFilterOperatorEnum): boolean { @@ -333,3 +373,9 @@ function getGraphQLOperator(operator: FieldFilterOperatorEnum): string { throw new Error(`Operation "${operator}" not supported`); } } + +// Function for getting 4 random digits to append to the clause variable name. +// Not used for any secrets or anything +function nonSecureRandomAlphaNumeric() { + return Math.random().toString(36).slice(2).slice(0, 4); +} diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts index 23023b25cc9..978a3383c15 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts @@ -24,12 +24,13 @@ import { TypeName, } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; +import { FilterClause } from '../types/filterClause'; import { buildFilterCondition, isOperatorAllowedForField, } from './filterBuilder'; -describe.skip('isOperatorAllowedForField', () => { +describe('isOperatorAllowedForField', () => { const testIsValidOperator = ( operator: FieldFilterOperatorEnum, fieldDef: IntrospectionField, @@ -126,15 +127,17 @@ describe('column filters', () => { name: string; introspectionFields: IntrospectionField[]; filter: Filter | undefined; - expectedResult: string; + expectedResult: string | any; + expectedFormattedValue: string | boolean | any; }; - describe.skip('empty filter testcases', () => { + describe('empty filter testcases', () => { const emptyFilterTestCases: FilterTestCase[] = [ { - name: 'returns empty string when filters are null or undefined', + name: 'returns empty object when filters are null or undefined', introspectionFields: [], filter: undefined, - expectedResult: '', + expectedResult: {}, + expectedFormattedValue: undefined, }, ]; emptyFilterTestCases.forEach( @@ -145,7 +148,7 @@ describe('column filters', () => { 'ProcessInstance', filter, ); - expect(result).toBe(expectedResult); + expect(result).toEqual(expectedResult); }); }, ); @@ -162,209 +165,249 @@ describe('column filters', () => { FieldFilterOperatorEnum.Eq, 'Hello World Workflow', ), - expectedResult: 'name: {equal: "Hello World Workflow"}', - }, - // { - // name: 'returns correct filter for single string field with like operator', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.Like, - // 'Hello%', - // ), - // expectedResult: 'name: {like: "Hello%"}', - // }, - // { - // name: 'returns correct filter for string field with isNull operator (true)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter('name', FieldFilterOperatorEnum.IsNull, true), - // expectedResult: 'name: {isNull: true}', - // }, - // { - // name: 'returns correct filter for string field with isNull operator (false)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.IsNull, - // false, - // ), - // expectedResult: 'name: {isNull: false}', - // }, - // { - // name: 'returns correct filter for string field with isNull operator ("true" as string)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.IsNull, - // 'True', - // ), - // expectedResult: 'name: {isNull: true}', - // }, - // { - // name: 'returns correct filter for string field with isNull operator ("false" as string)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.IsNull, - // 'FALSE', - // ), - // expectedResult: 'name: {isNull: false}', - // }, - // { - // name: 'returns correct filter for string field with in operator (single value)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ - // 'Test String', - // ]), - // expectedResult: 'name: {in: ["Test String"]}', - // }, - // { - // name: 'returns correct filter for string field with in operator (multiple values)', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ - // 'Test String 1', - // 'Test String 2', - // 'Test String 3', - // ]), - // expectedResult: - // 'name: {in: ["Test String 1", "Test String 2", "Test String 3"]}', - // }, - // { - // name: 'returns correct OR filter for two string fields with equal operator', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // createIntrospectionField('processName', TypeName.String), - // ], - // filter: { - // operator: 'OR', - // filters: [ - // createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.Eq, - // 'Hello World Workflow', - // ), - // createFieldFilter( - // 'processName', - // FieldFilterOperatorEnum.Eq, - // 'Greeting workflow', - // ), - // ], - // }, - // expectedResult: - // 'or: {name: {equal: "Hello World Workflow"}, processName: {equal: "Greeting workflow"}}', - // }, - // { - // name: 'returns correct filter for string field with like and isNull operators', - // introspectionFields: [ - // createIntrospectionField('description', TypeName.String), - // ], - // filter: { - // operator: 'OR', - // filters: [ - // createFieldFilter( - // 'description', - // FieldFilterOperatorEnum.Like, - // '%Test%', - // ), - // createFieldFilter( - // 'description', - // FieldFilterOperatorEnum.IsNull, - // true, - // ), - // ], - // }, - // expectedResult: - // 'or: {description: {like: "%Test%"}, description: {isNull: true}}', - // }, - // { - // name: 'returns correct filter for string field with in, like, equal, and isNull operators', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: { - // operator: 'OR', - // filters: [ - // createFieldFilter('name', FieldFilterOperatorEnum.In, [ - // 'Test String 1', - // 'Test String 2', - // ]), - // createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), - // createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.Eq, - // 'Exact Match', - // ), - // createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), - // ], - // }, - // expectedResult: - // 'or: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', - // }, - // { - // name: 'returns correct filter for string field with in, like, equal, and isNull operators', - // introspectionFields: [ - // createIntrospectionField('name', TypeName.String), - // ], - // filter: { - // operator: 'AND', - // filters: [ - // createFieldFilter('name', FieldFilterOperatorEnum.In, [ - // 'Test String 1', - // 'Test String 2', - // ]), - // createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), - // createFieldFilter( - // 'name', - // FieldFilterOperatorEnum.Eq, - // 'Exact Match', - // ), - // createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), - // ], - // }, - // expectedResult: - // 'and: {name: {in: ["Test String 1", "Test String 2"]}, name: {like: "%Test%"}, name: {equal: "Exact Match"}, name: {isNull: false}}', - // }, + expectedResult: 'name: {equal: $variable1}', + expectedFormattedValue: ['Hello World Workflow'], + }, + { + name: 'returns correct filter for single string field with like operator', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter( + 'name', + FieldFilterOperatorEnum.Like, + 'Hello%', + ), + expectedResult: 'name: {like: $variable1}', + expectedFormattedValue: ['Hello%'], + }, + { + name: 'returns correct filter for string field with isNull operator (true)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter('name', FieldFilterOperatorEnum.IsNull, true), + expectedResult: 'name: {isNull: $variable1}', + expectedFormattedValue: [true], + }, + { + name: 'returns correct filter for string field with isNull operator (false)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter( + 'name', + FieldFilterOperatorEnum.IsNull, + false, + ), + expectedResult: 'name: {isNull: $variable1}', + expectedFormattedValue: [false], + }, + { + name: 'returns correct filter for string field with isNull operator ("true" as string)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter( + 'name', + FieldFilterOperatorEnum.IsNull, + 'True', + ), + expectedResult: 'name: {isNull: $variable1}', + expectedFormattedValue: [true], + }, + { + name: 'returns correct filter for string field with isNull operator ("false" as string)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter( + 'name', + FieldFilterOperatorEnum.IsNull, + 'FALSE', + ), + expectedResult: 'name: {isNull: $variable1}', + expectedFormattedValue: [false], + }, + { + name: 'returns correct filter for string field with in operator (single value)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ + 'Test String', + ]), + expectedResult: 'name: {in: $variable1}', + expectedFormattedValue: [['Test String']], + }, + { + name: 'returns correct filter for string field with in operator (multiple values)', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: createFieldFilter('name', FieldFilterOperatorEnum.In, [ + 'Test String 1', + 'Test String 2', + 'Test String 3', + ]), + expectedResult: 'name: {in: $variable1}', + expectedFormattedValue: [ + ['Test String 1', 'Test String 2', 'Test String 3'], + ], + }, + { + name: 'returns correct OR filter for two string fields with equal operator', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + createIntrospectionField('processName', TypeName.String), + ], + filter: { + operator: 'OR', + filters: [ + createFieldFilter( + 'name', + FieldFilterOperatorEnum.Eq, + 'Hello World Workflow', + ), + createFieldFilter( + 'processName', + FieldFilterOperatorEnum.Eq, + 'Greeting workflow', + ), + ], + }, + expectedResult: + 'or: {name: {equal: $variable1}, processName: {equal: $variable2}}', + expectedFormattedValue: ['Hello World Workflow', 'Greeting workflow'], + }, + { + name: 'returns correct filter for string field with like and isNull operators', + introspectionFields: [ + createIntrospectionField('description', TypeName.String), + ], + filter: { + operator: 'OR', + filters: [ + createFieldFilter( + 'description', + FieldFilterOperatorEnum.Like, + '%Test%', + ), + createFieldFilter( + 'description', + FieldFilterOperatorEnum.IsNull, + true, + ), + ], + }, + expectedResult: + 'or: {description: {like: $variable1}, description: {isNull: $variable2}}', + expectedFormattedValue: ['%Test%', true], + }, + { + name: 'returns correct filter for string field with in, like, equal, and isNull operators', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: { + operator: 'OR', + filters: [ + createFieldFilter('name', FieldFilterOperatorEnum.In, [ + 'Test String 1', + 'Test String 2', + ]), + createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), + createFieldFilter( + 'name', + FieldFilterOperatorEnum.Eq, + 'Exact Match', + ), + createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), + ], + }, + expectedResult: + 'or: {name: {in: $variable1}, name: {like: $variable2}, name: {equal: $variable3}, name: {isNull: $variable4}}', + expectedFormattedValue: [ + ['Test String 1', 'Test String 2'], + '%Test%', + 'Exact Match', + false, + ], + }, + { + name: 'returns correct filter for string field with in, like, equal, and isNull operators', + introspectionFields: [ + createIntrospectionField('name', TypeName.String), + ], + filter: { + operator: 'AND', + filters: [ + createFieldFilter('name', FieldFilterOperatorEnum.In, [ + 'Test String 1', + 'Test String 2', + ]), + createFieldFilter('name', FieldFilterOperatorEnum.Like, '%Test%'), + createFieldFilter( + 'name', + FieldFilterOperatorEnum.Eq, + 'Exact Match', + ), + createFieldFilter('name', FieldFilterOperatorEnum.IsNull, false), + ], + }, + expectedResult: + 'and: {name: {in: $variable1}, name: {like: $variable2}, name: {equal: $variable3}, name: {isNull: $variable4}}', + expectedFormattedValue: [ + ['Test String 1', 'Test String 2'], + '%Test%', + 'Exact Match', + false, + ], + }, ]; stringTestCases.forEach( - ({ name, introspectionFields, filter, expectedResult }) => { + ({ + name, + introspectionFields, + filter, + expectedResult, + expectedFormattedValue, + }) => { it(`${name}`, () => { - const result = buildFilterCondition( + const result: FilterClause = buildFilterCondition( introspectionFields, 'ProcessInstance', filter, ); - expect(result).toBe(expectedResult); + expect(result).toBeDefined(); + let formattedClause = expectedResult; + result.clauseVariable.forEach((item, index) => { + formattedClause = formattedClause.replace( + `$variable${index + 1}`, + `$${item.clauseVariableName}`, + ); + expect(item.formattedValue).toEqual(expectedFormattedValue[index]); + }); + expect(formattedClause).toBe(result.clause); }); }, ); }); - describe.skip('idArgument testcases', () => { + describe('idArgument testcases', () => { const idTestCases: FilterTestCase[] = [ { name: 'returns correct filter for single id field with equal operator', introspectionFields: [createIntrospectionField('id', TypeName.Id)], filter: createFieldFilter('id', FieldFilterOperatorEnum.Eq, 'idA'), - expectedResult: 'id: {equal: "idA"}', + expectedResult: 'id: {equal: $variable1}', + expectedFormattedValue: ['idA'], }, { name: 'returns correct filter for single id field with isNull operator (false as boolean)', introspectionFields: [createIntrospectionField('id', TypeName.Id)], filter: createFieldFilter('id', FieldFilterOperatorEnum.IsNull, false), - expectedResult: 'id: {isNull: false}', + expectedResult: 'id: {isNull: $variable1}', + expectedFormattedValue: [false], }, { name: 'returns correct filter for single id field with isNull operator (false as string)', @@ -374,7 +417,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.IsNull, 'false', ), - expectedResult: 'id: {isNull: false}', + expectedResult: 'id: {isNull: $variable1}', + expectedFormattedValue: [false], }, { name: 'returns correct filter for single id field with IN operator', @@ -384,7 +428,8 @@ describe('column filters', () => { 'idB', 'idC', ]), - expectedResult: 'id: {in: ["idA", "idB", "idC"]}', + expectedResult: 'id: {in: $variable1}', + expectedFormattedValue: [['idA', 'idB', 'idC']], }, { name: 'returns correct OR filter for multiple id fields with equal, isNull, and IN operators', @@ -405,7 +450,8 @@ describe('column filters', () => { ], }, expectedResult: - 'or: {id: {equal: "idA"}, processId: {isNull: true}, id: {in: ["idA", "idB", "idC"]}}', + 'or: {id: {equal: $variable1}, processId: {isNull: $variable2}, id: {in: $variable3}}', + expectedFormattedValue: ['idA', true, ['idA', 'idB', 'idC']], }, { name: 'returns correct AND filter for multiple id fields with equal, isNull, and IN operators', @@ -426,24 +472,40 @@ describe('column filters', () => { ], }, expectedResult: - 'and: {id: {equal: "idA"}, processId: {isNull: true}, id: {in: ["idA", "idB", "idC"]}}', + 'and: {id: {equal: $variable1}, processId: {isNull: $variable2}, id: {in: $variable3}}', + expectedFormattedValue: ['idA', true, ['idA', 'idB', 'idC']], }, ]; idTestCases.forEach( - ({ name, introspectionFields, filter, expectedResult }) => { + ({ + name, + introspectionFields, + filter, + expectedResult, + expectedFormattedValue, + }) => { it(`${name}`, () => { const result = buildFilterCondition( introspectionFields, 'ProcessInstance', filter, ); - expect(result).toBe(expectedResult); + expect(result).toBeDefined(); + let formattedClause = expectedResult; + result.clauseVariable.forEach((item, index) => { + formattedClause = formattedClause.replace( + `$variable${index + 1}`, + `$${item.clauseVariableName}`, + ); + expect(item.formattedValue).toEqual(expectedFormattedValue[index]); + }); + expect(formattedClause).toBe(result.clause); }); }, ); }); - describe.skip('dateArgument testcases', () => { + describe('dateArgument testcases', () => { const testDate1 = '2024-10-10T09:54:40.759Z'; const testDate2 = '2025-10-10T09:54:40.759Z'; @@ -456,7 +518,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.Eq, testDate1, ), - expectedResult: `start: {equal: "${testDate1}"}`, + expectedResult: `start: {equal: $variable1}`, + expectedFormattedValue: [testDate1], }, { name: 'returns correct filter for single date field with isNull operator (false as boolean)', @@ -466,7 +529,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.IsNull, false, ), - expectedResult: 'start: {isNull: false}', + expectedResult: 'start: {isNull: $variable1}', + expectedFormattedValue: [false], }, { name: 'returns correct filter for single date field with isNull operator (false as string)', @@ -476,7 +540,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.IsNull, 'false', ), - expectedResult: 'start: {isNull: false}', + expectedResult: 'start: {isNull: $variable1}', + expectedFormattedValue: [false], }, { name: 'returns correct filter for single date field with GT operator', @@ -486,7 +551,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.Gt, testDate1, ), - expectedResult: `start: {greaterThan: "${testDate1}"}`, + expectedResult: `start: {greaterThan: $variable1}`, + expectedFormattedValue: [testDate1], }, { name: 'returns correct filter for single date field with GTE operator', @@ -496,7 +562,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.Gte, testDate1, ), - expectedResult: `start: {greaterThanEqual: "${testDate1}"}`, + expectedResult: `start: {greaterThanEqual: $variable1}`, + expectedFormattedValue: [testDate1], }, { name: 'returns correct filter for single date field with LT operator', @@ -506,7 +573,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.Lt, testDate1, ), - expectedResult: `start: {lessThan: "${testDate1}"}`, + expectedResult: `start: {lessThan: $variable1}`, + expectedFormattedValue: [testDate1], }, { name: 'returns correct filter for single date field with LTE operator', @@ -516,7 +584,8 @@ describe('column filters', () => { FieldFilterOperatorEnum.Lte, testDate1, ), - expectedResult: `start: {lessThanEqual: "${testDate1}"}`, + expectedResult: `start: {lessThanEqual: $variable1}`, + expectedFormattedValue: [testDate1], }, { name: 'returns correct filter for single date field with BETWEEN operator', @@ -525,7 +594,8 @@ describe('column filters', () => { testDate1, testDate2, ]), - expectedResult: `start: {between: {from: "${testDate1}", to: "${testDate2}"}}`, + expectedResult: `start: {between: {from: $variable1, to: $variable2}}`, + expectedFormattedValue: [testDate1, testDate2], }, { name: 'returns correct OR filter for multiple id fields with equal, isNull, and GT operators', @@ -541,7 +611,8 @@ describe('column filters', () => { createFieldFilter('end', FieldFilterOperatorEnum.Gt, testDate1), ], }, - expectedResult: `or: {start: {equal: "${testDate1}"}, end: {isNull: false}, end: {greaterThan: "${testDate1}"}}`, + expectedResult: `or: {start: {equal: $variable1}, end: {isNull: $variable2}, end: {greaterThan: $variable3}}`, + expectedFormattedValue: [testDate1, false, testDate1], }, { name: 'returns correct OR filter for multiple id fields with equal, isNull, and GTE operators', @@ -557,7 +628,8 @@ describe('column filters', () => { createFieldFilter('end', FieldFilterOperatorEnum.Gte, testDate1), ], }, - expectedResult: `or: {start: {equal: "${testDate1}"}, end: {isNull: false}, end: {greaterThanEqual: "${testDate1}"}}`, + expectedResult: `or: {start: {equal: $variable1}, end: {isNull: $variable2}, end: {greaterThanEqual: $variable3}}`, + expectedFormattedValue: [testDate1, false, testDate1], }, { name: 'returns correct AND filter for multiple id fields with equal, isNull, and LTE operators', @@ -573,7 +645,8 @@ describe('column filters', () => { createFieldFilter('end', FieldFilterOperatorEnum.Lte, testDate1), ], }, - expectedResult: `and: {start: {equal: "${testDate1}"}, end: {isNull: false}, end: {lessThanEqual: "${testDate1}"}}`, + expectedResult: `and: {start: {equal: $variable1}, end: {isNull: $variable2}, end: {lessThanEqual: $variable3}}`, + expectedFormattedValue: [testDate1, false, testDate1], }, { name: 'returns correct AND filter for multiple id fields with equal, isNull, LTE, and between operators', @@ -593,24 +666,46 @@ describe('column filters', () => { ]), ], }, - expectedResult: `and: {start: {equal: "${testDate1}"}, end: {isNull: false}, end: {lessThanEqual: "${testDate1}"}, start: {between: {from: "${testDate1}", to: "${testDate2}"}}}`, + expectedResult: `and: {start: {equal: $variable1}, end: {isNull: $variable2}, end: {lessThanEqual: $variable3}, start: {between: {from: $variable4, to: $variable5}}}`, + expectedFormattedValue: [ + testDate1, + false, + testDate1, + testDate1, + testDate2, + ], }, ]; idTestCases.forEach( - ({ name, introspectionFields, filter, expectedResult }) => { + ({ + name, + introspectionFields, + filter, + expectedResult, + expectedFormattedValue, + }) => { it(`${name}`, () => { const result = buildFilterCondition( introspectionFields, 'ProcessInstance', filter, ); - expect(result).toBe(expectedResult); + expect(result).toBeDefined(); + let formattedClause = expectedResult; + result.clauseVariable.forEach((item, index) => { + formattedClause = formattedClause.replace( + `$variable${index + 1}`, + `$${item.clauseVariableName}`, + ); + expect(item.formattedValue).toEqual(expectedFormattedValue[index]); + }); + expect(formattedClause).toBe(result.clause); }); }, ); }); - describe.skip('enumArgument testcases', () => { + describe('enumArgument testcases', () => { const idTestCases: FilterTestCase[] = [ { name: 'returns correct filter for state enum field with equal operator', @@ -622,19 +717,35 @@ describe('column filters', () => { FieldFilterOperatorEnum.Eq, ProcessInstanceStatusDTO.Completed, ), - expectedResult: `state: {equal: ${ProcessInstanceState.Completed}}`, + expectedResult: `state: {equal: $variable1}`, + expectedFormattedValue: ['COMPLETED'], }, ]; idTestCases.forEach( - ({ name, introspectionFields, filter, expectedResult }) => { + ({ + name, + introspectionFields, + filter, + expectedResult, + expectedFormattedValue, + }) => { it(`${name}`, () => { const result = buildFilterCondition( introspectionFields, 'ProcessInstance', filter, ); - expect(result).toBe(expectedResult); + expect(result).toBeDefined(); + let formattedClause = expectedResult; + result.clauseVariable.forEach((item, index) => { + formattedClause = formattedClause.replace( + `$variable${index + 1}`, + `$${item.clauseVariableName}`, + ); + expect(item.formattedValue).toEqual(expectedFormattedValue[index]); + }); + expect(formattedClause).toBe(result.clause); }); }, ); diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts index c40d07b806a..4841fea2fcd 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts @@ -14,20 +14,34 @@ * limitations under the License. */ +import { FilterClause } from '../types/filterClause'; import { Pagination, PaginationQueryVariable } from '../types/pagination'; -import { buildFilterCondition } from './filterBuilder'; export function buildGraphQlQuery(args: { type: 'ProcessDefinitions' | 'ProcessInstances' | 'Jobs'; queryBody: string; whereClause?: string; pagination?: Pagination; - filterCondition?: any; + filterCondition?: FilterClause; }): string { - // TODO: add variables to the top, https://graphql.org/learn/queries/#variables - let query = `query ($paginationInfo: Pagination, $orderByInfo: ${args.type.slice(0, -1)}OrderBy, $${args.filterCondition.clauseVariableName}: String){${args.type}`; + const queryHeaderStart = 'query ('; + const queryHeaderEnd = ')'; + const queryHeaderPaginationOrderByParams = `$paginationInfo: Pagination, $orderByInfo: ${args.type.slice(0, -1)}OrderBy`; + + const filterParams = args.filterCondition?.clauseVariable + ?.map(cl => { + return `$${cl.clauseVariableName}: ${cl.clauseVariableType}`; + }) + .join(', '); + + const params = [queryHeaderPaginationOrderByParams, filterParams] + .filter(Boolean) + .join(', '); + + let query = `${queryHeaderStart}${params}${queryHeaderEnd}{${args.type}`; const whereClause = buildWhereClause(args.whereClause); + const paginationClause = 'pagination: $paginationInfo'; const orderByClause = 'orderBy: $orderByInfo'; @@ -75,3 +89,19 @@ export function buildPaginationVariables( } return paginationVariable; } + +export function buildQueryParamVariable( + pagination?: Pagination, + filterCondition?: FilterClause, +) { + const paramVariables: any = { + paginationInfo: buildPaginationVariables(pagination), + orderByInfo: buildOrderByVariables(pagination), + }; + + filterCondition?.clauseVariable?.forEach(p => { + paramVariables[p.clauseVariableName] = p.formattedValue; + }); + + return paramVariables; +} diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index a1f61107fa4..2f097c0678a 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -33,8 +33,7 @@ import { ErrorBuilder } from '../helpers/errorBuilder'; import { buildFilterCondition } from '../helpers/filterBuilder'; import { buildGraphQlQuery, - buildOrderByVariables, - buildPaginationVariables, + buildQueryParamVariable, } from '../helpers/queryBuilder'; import { Pagination } from '../types/pagination'; import { FETCH_PROCESS_INSTANCES_SORT_FIELD } from './constants'; @@ -219,15 +218,6 @@ export class DataIndexService { ? `id: {in: ${JSON.stringify(definitionIds)}}` : undefined; - // TODO: something with filters - // We need to know how many filters there are so we can create those variables - // Then somehow add those to the top of the query - // Then add those to our variable list when exectuting the client - // TODO: the buildFilterCondition function needs to return an object that has the following: - // A list of the filters - // The fileters object should also include what the variable name will be and the value associated - // That list should be passed into the query builder function so then can get populated on the "header thing" - // That list will also get spread during the call to the query const filterCondition = filter ? buildFilterCondition( await this.initInputProcessDefinitionArgs(), @@ -236,19 +226,15 @@ export class DataIndexService { ) : undefined; - console.log(`Filter Condition: ${filterCondition}`); - let whereClause: string | undefined; if (definitionIdsCondition && filterCondition) { - whereClause = `and: [{${definitionIdsCondition}}, {${filterCondition.clause}}]`; + whereClause = `and: [{${definitionIdsCondition}}, {${filterCondition?.clause}}]`; } else if (definitionIdsCondition || filterCondition) { - whereClause = definitionIdsCondition ?? filterCondition; + whereClause = definitionIdsCondition ?? filterCondition?.clause; } else { whereClause = undefined; } - // console.log(`Where Clause: ${whereClause}`); - const graphQlQuery = buildGraphQlQuery({ type: 'ProcessDefinitions', queryBody: @@ -258,13 +244,12 @@ export class DataIndexService { filterCondition: filterCondition, }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); - // console.log(`GraphQL query: ${graphQlQuery}`); - // Spread the values here - const result = await this.client.query(graphQlQuery, { - paginationInfo: buildPaginationVariables(pagination), - orderByInfo: buildOrderByVariables(pagination), - [filterCondition.clauseVariableName]: filterCondition.formattedValue, - }); + + const paramVariables = buildQueryParamVariable(pagination, filterCondition); + + this.logger.debug(`GraphQL query params:`, paramVariables); + + const result = await this.client.query(graphQlQuery, paramVariables); this.logger.debug( `Get workflow definitions result: ${JSON.stringify(result)}`, ); @@ -297,9 +282,7 @@ export class DataIndexService { type, filter, ) - : ''; - - // console.log(`Filter Condition: ${filterCondition}`); + : undefined; let whereClause = ''; const conditions = []; @@ -312,8 +295,8 @@ export class DataIndexService { conditions.push(`{${definitionIdsCondition}}`); } - if (filter) { - conditions.push(`{${filterCondition}}`); + if (filterCondition) { + conditions.push(`{${filterCondition?.clause}}`); } if (conditions.length === 0) { @@ -324,7 +307,7 @@ export class DataIndexService { whereClause = `and: [${conditions.join(', ')}]`; } - // console.log(`Where Clause: ${whereClause}`); + console.log(`Where Clause: ${whereClause}`); const graphQlQuery = buildGraphQlQuery({ type: 'ProcessInstances', @@ -332,17 +315,18 @@ export class DataIndexService { 'id, processName, processId, state, start, end, nodes { id }, variables, executionSummary, parentProcessInstance {id, processName, businessKey}', whereClause, pagination, + filterCondition: filterCondition, }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); - // console.log(`GraphQL query: ${graphQlQuery}`); + + const paramVariables = buildQueryParamVariable(pagination, filterCondition); + + this.logger.debug(`GraphQL query params:`, paramVariables); const result = await this.client.query<{ ProcessInstances: ProcessInstance[]; - }>(graphQlQuery, { - paginationInfo: buildPaginationVariables(pagination), - orderByInfo: buildOrderByVariables(pagination), - }); + }>(graphQlQuery, paramVariables); this.logger.debug( `Fetch process instances result: ${JSON.stringify(result)}`, ); @@ -359,6 +343,7 @@ export class DataIndexService { return processInstances; } + // TODO: this has a NestedFilter public async fetchDefinitionIdsFromInstances(args: { targetEntity: string; }): Promise { @@ -375,16 +360,16 @@ export class DataIndexService { value: targetEntity, }, }; - + // TODO: construct the where clause and filter condition const filterCondition = buildFilterCondition( await this.inspectInputArgument(type), type, targetEntityFilter, ); - const whereClause = `and: [{${processIdNotNullCondition}}, {${filterCondition}}]`; + const whereClause = `and: [{${processIdNotNullCondition}}, {${filterCondition.clause}}]`; - // console.log(`Where Clause: ${whereClause}`); + console.log(`Where Clause: ${whereClause}`); // Apply a limit to prevent memory exhaustion and network timeouts when entities // have thousands of process instances. Entities with more instances than this limit @@ -399,16 +384,18 @@ export class DataIndexService { queryBody: 'processId', whereClause, pagination, + filterCondition, }); this.logger.debug(`GraphQL query: ${graphQlQuery}`); + const paramVariables = buildQueryParamVariable(pagination, filterCondition); + + this.logger.debug(`GraphQL query params:`, paramVariables); + const result = await this.client.query<{ ProcessInstances: ProcessInstance[]; - }>(graphQlQuery, { - paginationInfo: buildPaginationVariables(pagination), - orderByInfo: buildOrderByVariables(pagination), - }); + }>(graphQlQuery, paramVariables); this.logger.debug( `Fetch definition ids from instances history result: ${JSON.stringify(result)}`, ); diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts new file mode 100644 index 00000000000..a98db0bdbae --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts @@ -0,0 +1,26 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface FilterClauseVariable { + clauseVariableName: string; + formattedValue: string | boolean | any; + clauseVariableType: string; +} + +export interface FilterClause { + clauseVariable: Array; + clause: string; +} From 1d6c99b58433789931d92b190f5bc704dd6ece03 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Fri, 3 Apr 2026 12:51:30 -0400 Subject: [PATCH 07/18] squash: updating the tests --- .../src/helpers/filterBuilders.test.ts | 1 - .../src/service/DataIndexService.test.ts | 278 ++++++++++-------- .../src/service/DataIndexService.ts | 2 - 3 files changed, 163 insertions(+), 118 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts index 978a3383c15..2fb532a654f 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts @@ -18,7 +18,6 @@ import { FieldFilterOperatorEnum, Filter, IntrospectionField, - ProcessInstanceState, ProcessInstanceStatusDTO, TypeKind, TypeName, diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts index 05439c5a6f3..1b2cc327523 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.test.ts @@ -31,6 +31,7 @@ import { import * as buildGrahQLFilterUtils from '../helpers/filterBuilder'; import * as buildGrahQLQueryUtils from '../helpers/queryBuilder'; +import { FilterClause } from '../types/filterClause'; import { Pagination } from '../types/pagination'; import { mockProcessDefinitionArguments, @@ -85,11 +86,13 @@ const createQueryArgs = ( queryBody: string, whereClause?: string, pagination?: Pagination, + filterCondition?: FilterClause, ) => ({ type, queryBody, whereClause, pagination, + filterCondition, }); describe('initInputArgs', () => { @@ -180,9 +183,6 @@ describe('fetchWorkflowInfos', () => { 'id, name, version, type, endpoint, serviceUrl, source, metadata'; const pagination = { limit: 10, offset: 0, order: 'ASC', sortField: 'name' }; - const filterString = - 'or: {name: {equal: "Hello World Workflow"}, id: {equal: "yamlgreet"}}'; - const helloWorldFilter = { field: 'name', operator: FieldFilterOperatorEnum.Eq, @@ -357,12 +357,6 @@ describe('fetchWorkflowInfos', () => { ) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); - const expectedQueryArgs = createQueryArgs( - 'ProcessDefinitions', - queryBody, - filterString, - ); - // When const result = await dataIndexService.fetchWorkflowInfos({ filter: logicalFilter, @@ -372,33 +366,44 @@ describe('fetchWorkflowInfos', () => { expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessDefinitions); - expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(1); - expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ - type: 'ProcessDefinitions', - queryBody, - whereClause: filterString, - }); expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); expect(buildFilterConditionSpy).toHaveBeenCalledWith( mockProcessDefinitionIntrospection, 'ProcessDefinition', logicalFilter, ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(1); + expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ + type: 'ProcessDefinitions', + queryBody, + whereClause: createdFilter.clause, + filterCondition: createdFilter, + }); + + const expectedQueryArgs = createQueryArgs( + 'ProcessDefinitions', + queryBody, + createdFilter.clause, + undefined, + createdFilter, + ); + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + undefined, + createdFilter, + ); + expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: {}, - paginationInfo: {}, - }, + params, ); }); it('should fetch workflow infos with definitionIds and filter', async () => { - // Given - const whereClause = `and: [{id: {in: ${JSON.stringify( - definitionIds, - )}}}, {${filterString}}]`; // Given const mockQueryResult = { ProcessDefinitions: mockWfInfos, @@ -409,12 +414,6 @@ describe('fetchWorkflowInfos', () => { ) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); - const expectedQueryArgs = createQueryArgs( - 'ProcessDefinitions', - queryBody, - whereClause, - ); - // When const result = await dataIndexService.fetchWorkflowInfos({ definitionIds, @@ -422,6 +421,26 @@ describe('fetchWorkflowInfos', () => { }); // Then + expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); + expect(buildFilterConditionSpy).toHaveBeenCalledWith( + mockProcessDefinitionIntrospection, + 'ProcessDefinition', + logicalFilter, + ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + const whereClause = `and: [{id: {in: ${JSON.stringify( + definitionIds, + )}}}, {${createdFilter.clause}}]`; + + const expectedQueryArgs = createQueryArgs( + 'ProcessDefinitions', + queryBody, + whereClause, + undefined, + createdFilter, + ); expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(1); expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ @@ -429,20 +448,18 @@ describe('fetchWorkflowInfos', () => { queryBody: 'id, name, version, type, endpoint, serviceUrl, source, metadata', whereClause, + filterCondition: createdFilter, }); - expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); - expect(buildFilterConditionSpy).toHaveBeenCalledWith( - mockProcessDefinitionIntrospection, - 'ProcessDefinition', - logicalFilter, + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + undefined, + createdFilter, ); + expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: {}, - paginationInfo: {}, - }, + params, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessDefinitions); @@ -450,9 +467,7 @@ describe('fetchWorkflowInfos', () => { it('should fetch workflow infos with definitionIds, pagination, and filter', async () => { // Given - const whereClause = `and: [{id: {in: ${JSON.stringify( - definitionIds, - )}}}, {${filterString}}]`; + // Given const mockQueryResult = { ProcessDefinitions: mockWfInfos, @@ -463,12 +478,6 @@ describe('fetchWorkflowInfos', () => { ) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); - const expectedQueryArgs = createQueryArgs( - 'ProcessDefinitions', - queryBody, - whereClause, - pagination, - ); // When const result = await dataIndexService.fetchWorkflowInfos({ definitionIds, @@ -477,19 +486,36 @@ describe('fetchWorkflowInfos', () => { }); // Then + expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); + expect(buildFilterConditionSpy).toHaveBeenCalledWith( + mockProcessDefinitionIntrospection, + 'ProcessDefinition', + logicalFilter, + ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + const whereClause = `and: [{id: {in: ${JSON.stringify( + definitionIds, + )}}}, {${createdFilter.clause}}]`; + + const expectedQueryArgs = createQueryArgs( + 'ProcessDefinitions', + queryBody, + whereClause, + pagination, + createdFilter, + ); + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + pagination, + createdFilter, + ); expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: { - name: 'ASC', - }, - paginationInfo: { - limit: 10, - offset: 0, - }, - }, + params, ); expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(2); expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ @@ -497,13 +523,9 @@ describe('fetchWorkflowInfos', () => { queryBody, whereClause, pagination, + filterCondition: createdFilter, }); - expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); - expect(buildFilterConditionSpy).toHaveBeenCalledWith( - mockProcessDefinitionIntrospection, - 'ProcessDefinition', - logicalFilter, - ); + expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessDefinitions); }); @@ -541,9 +563,6 @@ describe('fetchInstances', () => { }, ]; - const filterString = - 'or: {processId: {equal: "processId1"}, processName: {like: "processName%"}}'; - const procName1Filter: FieldFilter = { field: 'processName', operator: FieldFilterOperatorEnum.Like, @@ -721,20 +740,34 @@ describe('fetchInstances', () => { it('should fetch instances with only filter', async () => { // Given - const whereClause = `and: [{${processIdNotNullCondition}}, {${filterString}}]`; + mockClient.query .mockResolvedValueOnce(mockOperationResult(mockProcessInstanceArguments)) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); + // When + const result = await dataIndexService.fetchInstances({ + filter: logicalFilter, + }); + + expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); + expect(buildFilterConditionSpy).toHaveBeenCalledWith( + mockProcessInstanceIntrospection, + 'ProcessInstance', + logicalFilter, + ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + const whereClause = `and: [{${processIdNotNullCondition}}, {${createdFilter.clause}}]`; + const expectedQueryArgs = createQueryArgs( 'ProcessInstances', queryBody, whereClause, + undefined, + createdFilter, ); - // When - const result = await dataIndexService.fetchInstances({ - filter: logicalFilter, - }); // Then expect(result).toBeDefined(); @@ -744,34 +777,26 @@ describe('fetchInstances', () => { type: 'ProcessInstances', queryBody, whereClause, + filterCondition: createdFilter, }); - expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); - expect(buildFilterConditionSpy).toHaveBeenCalledWith( - mockProcessInstanceIntrospection, - 'ProcessInstance', - logicalFilter, + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + undefined, + createdFilter, ); + expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: {}, - paginationInfo: {}, - }, + params, ); }); it('should fetch instances with definitionIds and filter', async () => { // Given - const whereClause = `and: [{${processIdNotNullCondition}}, {${processIdDefinitions}}}, {${filterString}}]`; mockClient.query .mockResolvedValueOnce(mockOperationResult(mockProcessInstanceArguments)) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); - const expectedQueryArgs = createQueryArgs( - 'ProcessInstances', - queryBody, - whereClause, - ); // When const result = await dataIndexService.fetchInstances({ definitionIds, @@ -779,25 +804,42 @@ describe('fetchInstances', () => { }); // Then + expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); + expect(buildFilterConditionSpy).toHaveBeenCalledWith( + mockProcessInstanceIntrospection, + 'ProcessInstance', + logicalFilter, + ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + const whereClause = `and: [{${processIdNotNullCondition}}, {${processIdDefinitions}}}, {${createdFilter.clause}}]`; + + const expectedQueryArgs = createQueryArgs( + 'ProcessInstances', + queryBody, + whereClause, + undefined, + createdFilter, + ); + expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(1); expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ type: 'ProcessInstances', queryBody, whereClause, + filterCondition: createdFilter, }); - expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); - expect(buildFilterConditionSpy).toHaveBeenCalledWith( - mockProcessInstanceIntrospection, - 'ProcessInstance', - logicalFilter, + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + undefined, + createdFilter, ); + expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: {}, - paginationInfo: {}, - }, + params, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessInstances); @@ -805,16 +847,10 @@ describe('fetchInstances', () => { it('should fetch instances with definitionIds, pagination, and filter', async () => { // Given - const whereClause = `and: [{${processIdNotNullCondition}}, {${processIdDefinitions}}}, {${filterString}}]`; mockClient.query .mockResolvedValueOnce(mockOperationResult(mockProcessInstanceArguments)) .mockResolvedValueOnce(mockOperationResult(mockQueryResult)); - const expectedQueryArgs = createQueryArgs( - 'ProcessInstances', - queryBody, - whereClause, - pagination, - ); + // When const result = await dataIndexService.fetchInstances({ definitionIds, @@ -823,31 +859,43 @@ describe('fetchInstances', () => { }); // Then + expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); + expect(buildFilterConditionSpy).toHaveBeenCalledWith( + mockProcessInstanceIntrospection, + 'ProcessInstance', + logicalFilter, + ); + + const createdFilter = buildFilterConditionSpy.mock.results[0].value; + + const whereClause = `and: [{${processIdNotNullCondition}}, {${processIdDefinitions}}}, {${createdFilter.clause}}]`; + + const expectedQueryArgs = createQueryArgs( + 'ProcessInstances', + queryBody, + whereClause, + pagination, + createdFilter, + ); + expect(buildGraphQlQuerySpy).toHaveBeenCalledTimes(1); expect(buildGraphQlQuerySpy).toHaveBeenCalledWith({ type: 'ProcessInstances', queryBody, whereClause, pagination, + filterCondition: createdFilter, }); - expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1); - expect(buildFilterConditionSpy).toHaveBeenCalledWith( - mockProcessInstanceIntrospection, - 'ProcessInstance', - logicalFilter, + + const params = buildGrahQLQueryUtils.buildQueryParamVariable( + pagination, + createdFilter, ); + expect(mockClient.query).toHaveBeenCalledTimes(2); expect(mockClient.query).toHaveBeenCalledWith( buildGrahQLQueryUtils.buildGraphQlQuery(expectedQueryArgs), - { - orderByInfo: { - name: 'ASC', - }, - paginationInfo: { - limit: 10, - offset: 0, - }, - }, + params, ); expect(result).toBeDefined(); expect(result).toStrictEqual(mockQueryResult.ProcessInstances); diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index 2f097c0678a..cb59c530873 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -307,8 +307,6 @@ export class DataIndexService { whereClause = `and: [${conditions.join(', ')}]`; } - console.log(`Where Clause: ${whereClause}`); - const graphQlQuery = buildGraphQlQuery({ type: 'ProcessInstances', queryBody: From 24e6c6388346a8f9da91860800f87377ce844727 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Fri, 3 Apr 2026 13:03:09 -0400 Subject: [PATCH 08/18] squash: better random alpha --- .../plugins/orchestrator-backend/src/helpers/filterBuilder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index 6d6a7de5023..bf056863b66 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -24,6 +24,8 @@ import { TypeName, } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; +import { randomBytes } from 'node:crypto'; + import { FilterClause, FilterClauseVariable } from '../types/filterClause'; type ProcessType = 'ProcessDefinition' | 'ProcessInstance'; @@ -377,5 +379,5 @@ function getGraphQLOperator(operator: FieldFilterOperatorEnum): string { // Function for getting 4 random digits to append to the clause variable name. // Not used for any secrets or anything function nonSecureRandomAlphaNumeric() { - return Math.random().toString(36).slice(2).slice(0, 4); + return randomBytes(8).toString('hex').slice(0, 4); } From 87925cb73361e0b1d7053ef8ca816f4e7f38f532 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Fri, 3 Apr 2026 13:13:06 -0400 Subject: [PATCH 09/18] squash: some code cleanup --- .../plugins/orchestrator-backend/src/helpers/filterBuilder.ts | 4 +--- .../orchestrator-backend/src/service/DataIndexService.ts | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index bf056863b66..721f76264b1 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -89,7 +89,6 @@ function handleLogicalFilter( clauseVariable: subClauses.flatMap(cl => cl.clauseVariable), }; return filterClause; - // return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`; } function handleNestedFilter( @@ -134,8 +133,7 @@ function handleBetweenOperator(filter: FieldFilter): FilterClause { const clause = `${filter.field}: {${getGraphQLOperator( FieldFilterOperatorEnum.Between, )}: {from: $${clauseVariableName1}, to: $${clauseVariableName2}}}`; - filterClauseVariableArray.push(filterClauseVariable1); - filterClauseVariableArray.push(filterClauseVariable2); + filterClauseVariableArray.push(filterClauseVariable1, filterClauseVariable2); const filterClause: FilterClause = { clause: clause, clauseVariable: filterClauseVariableArray, diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index cb59c530873..b041ed87793 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -341,7 +341,6 @@ export class DataIndexService { return processInstances; } - // TODO: this has a NestedFilter public async fetchDefinitionIdsFromInstances(args: { targetEntity: string; }): Promise { @@ -358,7 +357,7 @@ export class DataIndexService { value: targetEntity, }, }; - // TODO: construct the where clause and filter condition + const filterCondition = buildFilterCondition( await this.inspectInputArgument(type), type, From d1c9787acb7ac277c9d9bb28eac801306b6ce5c1 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Fri, 3 Apr 2026 13:19:26 -0400 Subject: [PATCH 10/18] squash: update the changeset --- workspaces/orchestrator/.changeset/five-meals-cover.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workspaces/orchestrator/.changeset/five-meals-cover.md b/workspaces/orchestrator/.changeset/five-meals-cover.md index 0e602ff9210..9dcd8eb7ee0 100644 --- a/workspaces/orchestrator/.changeset/five-meals-cover.md +++ b/workspaces/orchestrator/.changeset/five-meals-cover.md @@ -2,4 +2,5 @@ '@red-hat-developer-hub/backstage-plugin-orchestrator-backend': patch --- -Update dependecy @urql/core to fix CVE-2026-3118 +- Update dependecy @urql/core to fix CVE-2026-3118 +- Reworks the filter and query builder code to use query variables From 755e87be46d4e5ad29ed51d8780de2ce539c141b Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 10:13:44 -0400 Subject: [PATCH 11/18] squash: remove stray console.log --- .../orchestrator-backend/src/service/DataIndexService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index b041ed87793..246c3aac9ca 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -366,8 +366,6 @@ export class DataIndexService { const whereClause = `and: [{${processIdNotNullCondition}}, {${filterCondition.clause}}]`; - console.log(`Where Clause: ${whereClause}`); - // Apply a limit to prevent memory exhaustion and network timeouts when entities // have thousands of process instances. Entities with more instances than this limit // may not see all their associated workflows. From 81ffd88f90d254c583410ec4d3ea13fc9beeba4c Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 10:52:42 -0400 Subject: [PATCH 12/18] squash: some type cleanup and random function cleanup --- .../plugins/orchestrator-backend/src/helpers/filterBuilder.ts | 2 +- .../plugins/orchestrator-backend/src/types/filterClause.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index 721f76264b1..f33367dcfa5 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -377,5 +377,5 @@ function getGraphQLOperator(operator: FieldFilterOperatorEnum): string { // Function for getting 4 random digits to append to the clause variable name. // Not used for any secrets or anything function nonSecureRandomAlphaNumeric() { - return randomBytes(8).toString('hex').slice(0, 4); + return randomBytes(8).toString('hex'); } diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts index a98db0bdbae..a06183ebaac 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/types/filterClause.ts @@ -16,7 +16,7 @@ export interface FilterClauseVariable { clauseVariableName: string; - formattedValue: string | boolean | any; + formattedValue: string | boolean | string[]; clauseVariableType: string; } From 1de21c55258b541aecb2a2c1d1cd221b24ef9016 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 11:09:10 -0400 Subject: [PATCH 13/18] squash: fix negation sonar issu --- .../plugins/orchestrator-backend/src/helpers/queryBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts index 4841fea2fcd..5a4e4ea7d1c 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/queryBuilder.ts @@ -69,7 +69,7 @@ export function buildOrderByVariables(pagination?: Pagination): { if (pagination?.sortField !== undefined) { orderByVariable[pagination.sortField] = - pagination.order !== undefined ? pagination.order?.toUpperCase() : 'ASC'; + pagination.order?.toUpperCase() ?? 'ASC'; } return orderByVariable; From 9afe69866899e34677dddb29dd7c769d45ce3ec1 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 11:18:59 -0400 Subject: [PATCH 14/18] squash: more sonarcube fixes --- .../src/helpers/filterBuilders.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts index 2fb532a654f..56fd381f99c 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts @@ -126,7 +126,7 @@ describe('column filters', () => { name: string; introspectionFields: IntrospectionField[]; filter: Filter | undefined; - expectedResult: string | any; + expectedResult: string | FilterClause; expectedFormattedValue: string | boolean | any; }; describe('empty filter testcases', () => { @@ -135,7 +135,7 @@ describe('column filters', () => { name: 'returns empty object when filters are null or undefined', introspectionFields: [], filter: undefined, - expectedResult: {}, + expectedResult: {} as FilterClause, expectedFormattedValue: undefined, }, ]; @@ -379,7 +379,7 @@ describe('column filters', () => { filter, ); expect(result).toBeDefined(); - let formattedClause = expectedResult; + let formattedClause = expectedResult as string; result.clauseVariable.forEach((item, index) => { formattedClause = formattedClause.replace( `$variable${index + 1}`, @@ -491,7 +491,7 @@ describe('column filters', () => { filter, ); expect(result).toBeDefined(); - let formattedClause = expectedResult; + let formattedClause = expectedResult as string; result.clauseVariable.forEach((item, index) => { formattedClause = formattedClause.replace( `$variable${index + 1}`, @@ -691,7 +691,7 @@ describe('column filters', () => { filter, ); expect(result).toBeDefined(); - let formattedClause = expectedResult; + let formattedClause = expectedResult as string; result.clauseVariable.forEach((item, index) => { formattedClause = formattedClause.replace( `$variable${index + 1}`, @@ -736,7 +736,7 @@ describe('column filters', () => { filter, ); expect(result).toBeDefined(); - let formattedClause = expectedResult; + let formattedClause = expectedResult as string; result.clauseVariable.forEach((item, index) => { formattedClause = formattedClause.replace( `$variable${index + 1}`, From df6c68b4391ad186194090e09fa563c24111cace Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 11:31:46 -0400 Subject: [PATCH 15/18] squash . --- .../orchestrator-backend/src/helpers/filterBuilders.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts index 56fd381f99c..6befa390ed7 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts @@ -127,7 +127,7 @@ describe('column filters', () => { introspectionFields: IntrospectionField[]; filter: Filter | undefined; expectedResult: string | FilterClause; - expectedFormattedValue: string | boolean | any; + expectedFormattedValue: Array; }; describe('empty filter testcases', () => { const emptyFilterTestCases: FilterTestCase[] = [ @@ -136,7 +136,7 @@ describe('column filters', () => { introspectionFields: [], filter: undefined, expectedResult: {} as FilterClause, - expectedFormattedValue: undefined, + expectedFormattedValue: [''], }, ]; emptyFilterTestCases.forEach( From 95c5d7ea6601a763aa77251b1b65509d00d1b8c7 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 13:58:09 -0400 Subject: [PATCH 16/18] squash: use query variables on the other functions --- .../src/service/DataIndexService.ts | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index 246c3aac9ca..90e3377a019 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -135,9 +135,23 @@ export class DataIndexService { public async fetchWorkflowInfo( definitionId: string, ): Promise { - const graphQlQuery = `{ ProcessDefinitions ( where: {id: {equal: "${definitionId}" } } ) { id, name, version, type, endpoint, serviceUrl, source } }`; + const graphQlQuery = gql` + query FindProcessInstanceQuery($definitionId: String!) { + ProcessDefinitions(where: { id: { equal: $definitionId } }) { + id + name + version + type + endpoint + serviceUrl + source + } + } + `; - const result = await this.client.query(graphQlQuery, {}); + const result = await this.client.query(graphQlQuery, { + definitionId, + }); this.logger.debug( `Get workflow definition result: ${JSON.stringify(result)}`, @@ -423,9 +437,18 @@ export class DataIndexService { public async fetchWorkflowSource( definitionId: string, ): Promise { - const graphQlQuery = `{ ProcessDefinitions ( where: {id: {equal: "${definitionId}" } } ) { id, source } }`; + const graphQlQuery = gql` + query FindProcessInstanceQuery($definitionId: String!) { + ProcessDefinitions(where: { id: { equal: $definitionId } }) { + id + source + } + } + `; - const result = await this.client.query(graphQlQuery, {}); + const result = await this.client.query(graphQlQuery, { + definitionId, + }); this.logger.debug( `Fetch workflow source result: ${JSON.stringify(result)}`, @@ -453,12 +476,25 @@ export class DataIndexService { targetEntity?: string; }): Promise { const targetEntityWhereCondition = args.targetEntity - ? `, variables: {targetEntity: {equal: "${args.targetEntity}" } }` + ? `, variables: {targetEntity: {equal: $targetEntity } }` : ''; - const graphQlQuery = `{ ProcessInstances( where: {processId: {equal: "${args.definitionId}" } ${targetEntityWhereCondition} }, orderBy: {start:DESC}, pagination: {limit: ${args.limit}, offset: ${args.offset}}) { id, processName, state, start, end } }`; + const graphQlQuery = gql` + query FindProcessInstanceQuery($definitionId: String!, $limit: Int!, $offset: Int!) { + ProcessInstances( + where: {processId: {equal: $definitionId } ${targetEntityWhereCondition} }, + orderBy: {start:DESC}, + pagination: {limit: $limit, offset: $offset} + ) { id, processName, state, start, end } + } + `; - const result = await this.client.query(graphQlQuery, {}); + const result = await this.client.query(graphQlQuery, { + definitionId: args.definitionId, + limit: args.limit, + offset: args.offset, + targetEntity: args.targetEntity, + }); this.logger.debug( `Fetch workflow instances result: ${JSON.stringify(result)}`, @@ -475,9 +511,17 @@ export class DataIndexService { public async fetchInstanceVariables( instanceId: string, ): Promise { - const graphQlQuery = `{ ProcessInstances (where: { id: {equal: "${instanceId}" } } ) { variables } }`; + const graphQlQuery = gql` + query FindProcessInstanceQuery($instanceId: String!) { + ProcessInstances(where: { id: { equal: $instanceId } }) { + variables + } + } + `; - const result = await this.client.query(graphQlQuery, {}); + const result = await this.client.query(graphQlQuery, { + instanceId, + }); this.logger.debug( `Fetch process instance variables result: ${JSON.stringify(result)}`, @@ -500,9 +544,17 @@ export class DataIndexService { public async fetchDefinitionIdByInstanceId( instanceId: string, ): Promise { - const graphQlQuery = `{ ProcessInstances (where: { id: {equal: "${instanceId}" } } ) { processId } }`; + const graphQlQuery = gql` + query FindProcessInstanceQuery($instanceId: String!) { + ProcessInstances(where: { id: { equal: $instanceId } }) { + processId + } + } + `; - const result = await this.client.query(graphQlQuery, {}); + const result = await this.client.query(graphQlQuery, { + instanceId, + }); this.logger.debug( `Fetch process id from instance result: ${JSON.stringify(result)}`, From 1620db144fd954edf0b37ef3f64bf6b8eda4aae0 Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Tue, 7 Apr 2026 14:20:51 -0400 Subject: [PATCH 17/18] squash: fix query if an enum with state --- .../plugins/orchestrator-backend/src/helpers/filterBuilder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts index f33367dcfa5..584cafe7146 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/helpers/filterBuilder.ts @@ -198,7 +198,9 @@ function handleBinaryOperator( formattedValue = binaryFilter.value.map(v => formatValue(binaryFilter.field, v, fieldDef, type), ); - paramType = '[String!]'; + paramType = isEnumFilter(binaryFilter.field, type) + ? '[ProcessInstanceState!]' + : '[String!]'; } else { formattedValue = formatValue( binaryFilter.field, From 07724c0964233a6913549fb3d3e41b3ef1ee5ede Mon Sep 17 00:00:00 2001 From: Lucas Holmquist Date: Wed, 8 Apr 2026 10:52:39 -0400 Subject: [PATCH 18/18] squash: fix issue with targetEntity query --- .../src/service/DataIndexService.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts index 90e3377a019..c935bc4ce2d 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts +++ b/workspaces/orchestrator/plugins/orchestrator-backend/src/service/DataIndexService.ts @@ -480,7 +480,7 @@ export class DataIndexService { : ''; const graphQlQuery = gql` - query FindProcessInstanceQuery($definitionId: String!, $limit: Int!, $offset: Int!) { + query FindProcessInstanceQuery($definitionId: String!, $limit: Int!, $offset: Int! ${args.targetEntity ? `, $targetEntity: String` : ''}) { ProcessInstances( where: {processId: {equal: $definitionId } ${targetEntityWhereCondition} }, orderBy: {start:DESC}, @@ -489,12 +489,21 @@ export class DataIndexService { } `; - const result = await this.client.query(graphQlQuery, { - definitionId: args.definitionId, - limit: args.limit, - offset: args.offset, - targetEntity: args.targetEntity, - }); + const result = await this.client.query( + graphQlQuery, + args.targetEntity + ? { + definitionId: args.definitionId, + limit: args.limit, + offset: args.offset, + targetEntity: args.targetEntity, + } + : { + definitionId: args.definitionId, + limit: args.limit, + offset: args.offset, + }, + ); this.logger.debug( `Fetch workflow instances result: ${JSON.stringify(result)}`,