Skip to content

Commit

Permalink
feat(orchestrator): add filter by enum (#2453)
Browse files Browse the repository at this point in the history
* rm unsed introspection query

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>

* update openapi spec

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>

* fix function name

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>

* filter by enum

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>

* Changeset

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>

---------

Signed-off-by: Gloria Ciavarrini <gciavarrini@redhat.com>
  • Loading branch information
gciavarrini authored Nov 10, 2024
1 parent 0f065d5 commit 25f1787
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .changeset/neat-pigs-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@janus-idp/backstage-plugin-orchestrator-backend": minor
"@janus-idp/backstage-plugin-orchestrator-common": minor
---

Add enum filters to orchestrator plugin
78 changes: 72 additions & 6 deletions plugins/orchestrator-backend/src/helpers/filterBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@ import {
Filter,
IntrospectionField,
LogicalFilter,
ProcessInstanceStatusDTO,
TypeName,
} from '@janus-idp/backstage-plugin-orchestrator-common';

import { getProcessInstanceStateFromStatusDTOString } from '../service/api/mapping/V2Mappings';

type ProcessType = 'ProcessDefinition' | 'ProcessInstance';

function isLogicalFilter(filter: Filter): filter is LogicalFilter {
return (filter as LogicalFilter).filters !== undefined;
}

function handleLogicalFilter(
introspection: IntrospectionField[],
type: ProcessType,
filter: LogicalFilter,
): string {
if (!filter.operator) return '';

const subClauses = filter.filters.map(f =>
buildFilterCondition(introspection, f),
buildFilterCondition(introspection, type, f),
);

return `${filter.operator.toLowerCase()}: {${subClauses.join(', ')}}`;
Expand All @@ -35,27 +41,83 @@ function handleIsNullOperator(filter: FieldFilter): string {
return `${filter.field}: {${getGraphQLOperator(FieldFilterOperatorEnum.IsNull)}: ${convertToBoolean(filter.value)}}`;
}

function isEnumFilter(
fieldName: string,
type: 'ProcessDefinition' | 'ProcessInstance',
): boolean {
if (type === 'ProcessInstance') {
if (fieldName === 'state') {
return true;
}
}
return false;
}

function convertEnumValue(
fieldName: string,
fieldValue: string,
type: 'ProcessDefinition' | 'ProcessInstance',
): string {
if (type === 'ProcessInstance') {
if (fieldName === 'state') {
const state = (ProcessInstanceStatusDTO as any)[
fieldValue as keyof typeof ProcessInstanceStatusDTO
];

if (!state) {
throw new Error(
`status ${fieldValue} is not a valid value of ProcessInstanceStatusDTO`,
);
}
return getProcessInstanceStateFromStatusDTOString(state).valueOf();
}
}
throw new Error(
`Unsupported enum ${fieldName}: can't convert value ${fieldValue}`,
);
}

function isValidEnumOperator(operator: FieldFilterOperatorEnum): boolean {
return (
operator === FieldFilterOperatorEnum.In ||
operator === FieldFilterOperatorEnum.Eq
);
}

function handleBinaryOperator(
binaryFilter: FieldFilter,
fieldDef: IntrospectionField,
type: 'ProcessDefinition' | 'ProcessInstance',
): string {
if (isEnumFilter(binaryFilter.field, type)) {
if (!isValidEnumOperator(binaryFilter.operator)) {
throw new Error(
`Invalid operator ${binaryFilter.operator} for enum field ${binaryFilter.field} filter`,
);
}
binaryFilter.value = convertEnumValue(
binaryFilter.field,
binaryFilter.value,
type,
);
}
const formattedValue = Array.isArray(binaryFilter.value)
? `[${binaryFilter.value.map(v => formatValue(binaryFilter.field, v, fieldDef)).join(', ')}]`
: formatValue(binaryFilter.field, binaryFilter.value, fieldDef);

? `[${binaryFilter.value.map(v => formatValue(binaryFilter.field, v, fieldDef, type)).join(', ')}]`
: formatValue(binaryFilter.field, binaryFilter.value, fieldDef, type);
return `${binaryFilter.field}: {${getGraphQLOperator(binaryFilter.operator)}: ${formattedValue}}`;
}

export function buildFilterCondition(
introspection: IntrospectionField[],
type: ProcessType,
filters?: Filter,
): string {
if (!filters) {
return '';
}

if (isLogicalFilter(filters)) {
return handleLogicalFilter(introspection, filters);
return handleLogicalFilter(introspection, type, filters);
}

if (!isOperatorSupported(filters.operator)) {
Expand Down Expand Up @@ -83,7 +145,7 @@ export function buildFilterCondition(
case FieldFilterOperatorEnum.Gte:
case FieldFilterOperatorEnum.Lt:
case FieldFilterOperatorEnum.Lte:
return handleBinaryOperator(filters, fieldDef);
return handleBinaryOperator(filters, fieldDef, type);

default:
throw new Error(`Can't build filter condition`);
Expand Down Expand Up @@ -156,11 +218,15 @@ function formatValue(
fieldName: string,
fieldValue: any,
fieldDef: IntrospectionField,
type: ProcessType,
): string {
if (!isFieldFilterSupported) {
throw new Error(`Unsupported field type ${fieldDef.type.name}`);
}

if (isEnumFilter(fieldName, type)) {
return `${fieldValue}`;
}
if (
fieldDef.type.name === TypeName.String ||
fieldDef.type.name === TypeName.Id ||
Expand Down
55 changes: 51 additions & 4 deletions plugins/orchestrator-backend/src/helpers/filterBuilders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
FieldFilterOperatorEnum,
Filter,
IntrospectionField,
ProcessInstanceState,
ProcessInstanceStatusDTO,
TypeKind,
TypeName,
} from '@janus-idp/backstage-plugin-orchestrator-common';
Expand Down Expand Up @@ -49,7 +51,11 @@ describe('column filters', () => {
emptyFilterTestCases.forEach(
({ name, introspectionFields, filter, expectedResult }) => {
it(`${name}`, () => {
const result = buildFilterCondition(introspectionFields, filter);
const result = buildFilterCondition(
introspectionFields,
'ProcessInstance',
filter,
);
expect(result).toBe(expectedResult);
});
},
Expand Down Expand Up @@ -247,7 +253,11 @@ describe('column filters', () => {
stringTestCases.forEach(
({ name, introspectionFields, filter, expectedResult }) => {
it(`${name}`, () => {
const result = buildFilterCondition(introspectionFields, filter);
const result = buildFilterCondition(
introspectionFields,
'ProcessInstance',
filter,
);
expect(result).toBe(expectedResult);
});
},
Expand Down Expand Up @@ -334,7 +344,11 @@ describe('column filters', () => {
idTestCases.forEach(
({ name, introspectionFields, filter, expectedResult }) => {
it(`${name}`, () => {
const result = buildFilterCondition(introspectionFields, filter);
const result = buildFilterCondition(
introspectionFields,
'ProcessInstance',
filter,
);
expect(result).toBe(expectedResult);
});
},
Expand Down Expand Up @@ -497,7 +511,40 @@ describe('column filters', () => {
idTestCases.forEach(
({ name, introspectionFields, filter, expectedResult }) => {
it(`${name}`, () => {
const result = buildFilterCondition(introspectionFields, filter);
const result = buildFilterCondition(
introspectionFields,
'ProcessInstance',
filter,
);
expect(result).toBe(expectedResult);
});
},
);
});
describe('enumArgument testcases', () => {
const idTestCases: FilterTestCase[] = [
{
name: 'returns correct filter for state enum field with equal operator',
introspectionFields: [
createIntrospectionField('state', TypeName.String),
],
filter: createFieldFilter(
'state',
FieldFilterOperatorEnum.Eq,
ProcessInstanceStatusDTO.Completed,
),
expectedResult: `state: {equal: ${ProcessInstanceState.Completed}}`,
},
];

idTestCases.forEach(
({ name, introspectionFields, filter, expectedResult }) => {
it(`${name}`, () => {
const result = buildFilterCondition(
introspectionFields,
'ProcessInstance',
filter,
);
expect(result).toBe(expectedResult);
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { Client, OperationResult } from '@urql/core';

import {
FieldFilter,
FieldFilterOperatorEnum,
LogicalFilter,
NodeInstance,
Expand Down Expand Up @@ -349,6 +350,7 @@ describe('fetchWorkflowInfos', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessDefinitionIntrospection,
'ProcessDefinition',
logicalFilter,
);
expect(mockClient.query).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -394,6 +396,7 @@ describe('fetchWorkflowInfos', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessDefinitionIntrospection,
'ProcessDefinition',
logicalFilter,
);
expect(mockClient.query).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -448,6 +451,7 @@ describe('fetchWorkflowInfos', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessDefinitionIntrospection,
'ProcessDefinition',
logicalFilter,
);
expect(result).toBeDefined();
Expand Down Expand Up @@ -488,12 +492,12 @@ describe('fetchInstances', () => {
const filterString =
'or: {processId: {equal: "processId1"}, processName: {like: "processName%"}}';

const procName1Filter = {
const procName1Filter: FieldFilter = {
field: 'processName',
operator: FieldFilterOperatorEnum.Like,
value: 'processName%',
};
const procId1Filter = {
const procId1Filter: FieldFilter = {
field: 'processId',
operator: FieldFilterOperatorEnum.Eq,
value: 'processId1',
Expand Down Expand Up @@ -678,6 +682,7 @@ describe('fetchInstances', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessInstanceIntrospection,
'ProcessInstance',
logicalFilter,
);
expect(mockClient.query).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -714,6 +719,7 @@ describe('fetchInstances', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessInstanceIntrospection,
'ProcessInstance',
logicalFilter,
);
expect(mockClient.query).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -755,6 +761,7 @@ describe('fetchInstances', () => {
expect(buildFilterConditionSpy).toHaveBeenCalledTimes(1);
expect(buildFilterConditionSpy).toHaveBeenCalledWith(
mockProcessInstanceIntrospection,
'ProcessInstance',
logicalFilter,
);
expect(mockClient.query).toHaveBeenCalledTimes(2);
Expand Down
44 changes: 6 additions & 38 deletions plugins/orchestrator-backend/src/service/DataIndexService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
fromWorkflowSource,
getWorkflowCategory,
IntrospectionField,
IntrospectionQuery,
parseWorkflowVariables,
ProcessInstance,
WorkflowDefinition,
Expand Down Expand Up @@ -79,6 +78,7 @@ export class DataIndexService {
}
}`;
}

public async inspectInputArgument(
type: string,
): Promise<IntrospectionField[]> {
Expand Down Expand Up @@ -113,42 +113,6 @@ export class DataIndexService {
return pairs;
}

public async getSchemaTypes(type: string): Promise<IntrospectionQuery> {
const graphQlQuery = `query IntrospectionQuery {
__type(name: "${type}") {
name
kind
description
fields {
name
type {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
`;

const result = await this.client.query(graphQlQuery, {});

this.logger.debug(`Introspection query result: ${JSON.stringify(result)}`);

if (result.error) {
this.logger.error(`Error executing introspection query ${result.error}`);
throw result.error;
}
return result as unknown as IntrospectionQuery;
}

public async abortWorkflowInstance(instanceId: string): Promise<void> {
this.logger.info(`Aborting workflow instance ${instanceId}`);
const ProcessInstanceAbortMutationDocument = gql`
Expand Down Expand Up @@ -237,6 +201,7 @@ export class DataIndexService {
const filterCondition = filter
? buildFilterCondition(
await this.initInputProcessDefinitionArgs(),
'ProcessDefinition',
filter,
)
: undefined;
Expand Down Expand Up @@ -284,9 +249,11 @@ export class DataIndexService {
const definitionIdsCondition = definitionIds
? `processId: {in: ${JSON.stringify(definitionIds)}}`
: undefined;
const type = 'ProcessInstance';
const filterCondition = filter
? buildFilterCondition(
await this.inspectInputArgument('ProcessInstance'),
await this.inspectInputArgument(type),
type,
filter,
)
: '';
Expand Down Expand Up @@ -352,6 +319,7 @@ export class DataIndexService {
const filterCondition = filter
? buildFilterCondition(
await this.inspectInputArgument('ProcessInstance'),
'ProcessInstance',
filter,
)
: '';
Expand Down
Loading

0 comments on commit 25f1787

Please sign in to comment.