Refactor graphql query runner + fix nested or (#6986)

This commit is contained in:
Weiko
2024-09-11 14:22:33 +02:00
committed by GitHub
parent b506332420
commit 725ee837f9
8 changed files with 340 additions and 264 deletions

View File

@ -17,4 +17,6 @@ export enum GraphqlQueryRunnerExceptionCode {
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
} }

View File

@ -80,7 +80,8 @@ describe('GraphqlQueryFilterOperatorParser', () => {
it('should parse is operator with non-NULL value correctly', () => { it('should parse is operator with non-NULL value correctly', () => {
const result = parser.parseOperator({ is: 'NOT_NULL' }, false); const result = parser.parseOperator({ is: 'NOT_NULL' }, false);
expect(result).toBe('NOT_NULL'); expect(result).toBeInstanceOf(FindOperator);
expect(result).toEqual(Not(IsNull()));
}); });
it('should parse like operator correctly', () => { it('should parse like operator correctly', () => {

View File

@ -26,16 +26,22 @@ export class GraphqlQueryFilterConditionParser {
} }
const result: FindOptionsWhere<ObjectLiteral> = {}; const result: FindOptionsWhere<ObjectLiteral> = {};
let orCondition: FindOptionsWhere<ObjectLiteral>[] | null = null;
for (const [key, value] of Object.entries(conditions)) { for (const [key, value] of Object.entries(conditions)) {
switch (key) { switch (key) {
case 'and': case 'and': {
Object.assign(result, this.parseAndCondition(value, isNegated)); const andConditions = this.parseAndCondition(value, isNegated);
break;
case 'or': return andConditions.map((condition) => ({
orCondition = this.parseOrCondition(value, isNegated); ...result,
break; ...condition,
}));
}
case 'or': {
const orConditions = this.parseOrCondition(value, isNegated);
return orConditions.map((condition) => ({ ...result, ...condition }));
}
case 'not': case 'not':
Object.assign(result, this.parse(value, !isNegated)); Object.assign(result, this.parse(value, !isNegated));
break; break;
@ -47,10 +53,6 @@ export class GraphqlQueryFilterConditionParser {
} }
} }
if (orCondition) {
return orCondition.map((condition) => ({ ...result, ...condition }));
}
return result; return result;
} }
@ -84,32 +86,28 @@ export class GraphqlQueryFilterConditionParser {
combineType: 'and' | 'or', combineType: 'and' | 'or',
): FindOptionsWhere<ObjectLiteral>[] { ): FindOptionsWhere<ObjectLiteral>[] {
if (combineType === 'and') { if (combineType === 'and') {
let result: FindOptionsWhere<ObjectLiteral>[] = [{}]; return conditions.reduce<FindOptionsWhere<ObjectLiteral>[]>(
(acc, condition) => {
for (const condition of conditions) { if (Array.isArray(condition)) {
if (Array.isArray(condition)) { return acc.flatMap((accCondition) =>
const newResult: FindOptionsWhere<ObjectLiteral>[] = []; condition.map((subCondition) => ({
...accCondition,
for (const existingCondition of result) { ...subCondition,
for (const orCondition of condition) { })),
newResult.push({ );
...existingCondition,
...orCondition,
});
}
} }
result = newResult;
} else { return acc.map((accCondition) => ({
result = result.map((existingCondition) => ({ ...accCondition,
...existingCondition,
...condition, ...condition,
})); }));
} },
} [{}],
);
return result;
} }
return conditions.flat(); return conditions.flatMap((condition) =>
Array.isArray(condition) ? condition : [condition],
);
} }
} }

View File

@ -1,9 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
import { FindManyOptions, ObjectLiteral } from 'typeorm';
import { import {
Record as IRecord, Record as IRecord,
RecordFilter, RecordFilter,
@ -16,16 +12,8 @@ import {
FindOneResolverArgs, FindOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -43,48 +31,10 @@ export class GraphqlQueryRunnerService {
args: FindOneResolverArgs<Filter>, args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> { ): Promise<ObjectRecord | undefined> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } = const graphqlQueryFindOneResolverService =
options; new GraphqlQueryFindOneResolverService(this.twentyORMGlobalManager);
const repository = await this.getRepository(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
objectMetadataCollection,
);
const objectMetadata = this.getObjectMetadata(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMap,
);
const { select, relations } = graphqlQueryParser.parseSelectedFields( return graphqlQueryFindOneResolverService.findOne(args, options);
objectMetadataItem,
graphqlFields(info),
);
const where = graphqlQueryParser.parseFilter(args.filter ?? ({} as Filter));
const objectRecord = await repository.findOne({ where, select, relations });
if (!objectRecord) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord(
objectRecord,
objectMetadataItem.nameSingular,
1,
1,
) as ObjectRecord;
} }
@LogExecutionTime() @LogExecutionTime()
@ -96,180 +46,9 @@ export class GraphqlQueryRunnerService {
args: FindManyResolverArgs<Filter, OrderBy>, args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> { ): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } = const graphqlQueryFindManyResolverService =
options; new GraphqlQueryFindManyResolverService(this.twentyORMGlobalManager);
this.validateArgsOrThrow(args); return graphqlQueryFindManyResolverService.findMany(args, options);
const repository = await this.getRepository(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
objectMetadataCollection,
);
const objectMetadata = this.getObjectMetadata(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMap,
);
const { select, relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
graphqlFields(info),
);
const isForwardPagination = !isDefined(args.before);
const order = graphqlQueryParser.parseOrder(
args.orderBy ?? [],
isForwardPagination,
);
const where = graphqlQueryParser.parseFilter(
args.filter ?? ({} as Filter),
true,
);
const cursor = this.getCursor(args);
const limit = this.getLimit(args);
this.addOrderByColumnsToSelect(order, select);
const findOptions: FindManyOptions<ObjectLiteral> = {
where,
order,
select,
relations,
take: limit + 1,
};
const totalCount = await repository.count({ where });
if (cursor) {
applyRangeFilter(where, cursor, isForwardPagination);
}
const objectRecords = await repository.find(findOptions);
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
objectRecords,
limit,
isForwardPagination,
);
if (objectRecords.length > limit) {
objectRecords.pop();
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
return typeORMObjectRecordsParser.createConnection(
objectRecords as ObjectRecord[],
objectMetadataItem.nameSingular,
limit,
totalCount,
order,
hasNextPage,
hasPreviousPage,
);
}
private async getRepository(workspaceId: string, objectName: string) {
return this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
objectName,
);
}
private getObjectMetadata(
objectMetadataMap: Record<string, any>,
objectName: string,
) {
const objectMetadata = objectMetadataMap[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectName}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return objectMetadata;
}
private getCursor(
args: FindManyResolverArgs<any, any>,
): Record<string, any> | undefined {
if (args.after) return decodeCursor(args.after);
if (args.before) return decodeCursor(args.before);
return undefined;
}
private getLimit(args: FindManyResolverArgs<any, any>): number {
return args.first ?? args.last ?? QUERY_MAX_RECORDS;
}
private addOrderByColumnsToSelect(
order: Record<string, any>,
select: Record<string, boolean>,
) {
for (const column of Object.keys(order || {})) {
if (!select[column]) {
select[column] = true;
}
}
}
private getPaginationInfo(
objectRecords: any[],
limit: number,
isForwardPagination: boolean,
) {
const hasMoreRecords = objectRecords.length > limit;
return {
hasNextPage: isForwardPagination && hasMoreRecords,
hasPreviousPage: !isForwardPagination && hasMoreRecords,
};
}
private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) {
if (args.first && args.last) {
throw new GraphqlQueryRunnerException(
'Cannot provide both first and last',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.after) {
throw new GraphqlQueryRunnerException(
'Cannot provide both before and after',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.first) {
throw new GraphqlQueryRunnerException(
'Cannot provide both before and first',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.after && args.last) {
throw new GraphqlQueryRunnerException(
'Cannot provide both after and last',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.first !== undefined && args.first < 0) {
throw new GraphqlQueryRunnerException(
'First argument must be non-negative',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
if (args.last !== undefined && args.last < 0) {
throw new GraphqlQueryRunnerException(
'Last argument must be non-negative',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
} }
} }

View File

@ -0,0 +1,194 @@
import { isDefined } from 'class-validator';
import graphqlFields from 'graphql-fields';
import { FindManyOptions, ObjectLiteral } from 'typeorm';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
import {
convertObjectMetadataToMap,
getObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQueryFindManyResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async findMany<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<ObjectRecord>> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
options;
this.validateArgsOrThrow(args);
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadata(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMap,
);
const { select, relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
graphqlFields(info),
);
const isForwardPagination = !isDefined(args.before);
const order = graphqlQueryParser.parseOrder(
args.orderBy ?? [],
isForwardPagination,
);
const where = graphqlQueryParser.parseFilter(
args.filter ?? ({} as Filter),
true,
);
const cursor = this.getCursor(args);
const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS;
this.addOrderByColumnsToSelect(order, select);
const findOptions: FindManyOptions<ObjectLiteral> = {
where,
order,
select,
relations,
take: limit + 1,
};
const totalCount = await repository.count({ where });
if (cursor) {
applyRangeFilter(where, cursor, isForwardPagination);
}
const objectRecords = await repository.find(findOptions);
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
objectRecords,
limit,
isForwardPagination,
);
if (objectRecords.length > limit) {
objectRecords.pop();
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
return typeORMObjectRecordsParser.createConnection(
objectRecords as ObjectRecord[],
objectMetadataItem.nameSingular,
limit,
totalCount,
order,
hasNextPage,
hasPreviousPage,
);
}
private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) {
if (args.first && args.last) {
throw new GraphqlQueryRunnerException(
'Cannot provide both first and last',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.after) {
throw new GraphqlQueryRunnerException(
'Cannot provide both before and after',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.first) {
throw new GraphqlQueryRunnerException(
'Cannot provide both before and first',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.after && args.last) {
throw new GraphqlQueryRunnerException(
'Cannot provide both after and last',
GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.first !== undefined && args.first < 0) {
throw new GraphqlQueryRunnerException(
'First argument must be non-negative',
GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST,
);
}
if (args.last !== undefined && args.last < 0) {
throw new GraphqlQueryRunnerException(
'Last argument must be non-negative',
GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST,
);
}
}
private getCursor(
args: FindManyResolverArgs<any, any>,
): Record<string, any> | undefined {
if (args.after) return decodeCursor(args.after);
if (args.before) return decodeCursor(args.before);
return undefined;
}
private addOrderByColumnsToSelect(
order: Record<string, any>,
select: Record<string, boolean>,
) {
for (const column of Object.keys(order || {})) {
if (!select[column]) {
select[column] = true;
}
}
}
private getPaginationInfo(
objectRecords: any[],
limit: number,
isForwardPagination: boolean,
) {
const hasMoreRecords = objectRecords.length > limit;
return {
hasNextPage: isForwardPagination && hasMoreRecords,
hasPreviousPage: !isForwardPagination && hasMoreRecords,
};
}
}

View File

@ -0,0 +1,80 @@
import graphqlFields from 'graphql-fields';
import {
Record as IRecord,
RecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import {
convertObjectMetadataToMap,
getObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQueryFindOneResolverService {
private twentyORMGlobalManager: TwentyORMGlobalManager;
constructor(twentyORMGlobalManager: TwentyORMGlobalManager) {
this.twentyORMGlobalManager = twentyORMGlobalManager;
}
async findOne<
ObjectRecord extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
options;
const repository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadata(
objectMetadataMap,
objectMetadataItem.nameSingular,
);
const graphqlQueryParser = new GraphqlQueryParser(
objectMetadata.fields,
objectMetadataMap,
);
const { select, relations } = graphqlQueryParser.parseSelectedFields(
objectMetadataItem,
graphqlFields(info),
);
const where = graphqlQueryParser.parseFilter(args.filter ?? ({} as Filter));
const objectRecord = await repository.findOne({ where, select, relations });
if (!objectRecord) {
throw new GraphqlQueryRunnerException(
'Record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
return typeORMObjectRecordsParser.processRecord(
objectRecord,
objectMetadataItem.nameSingular,
1,
1,
) as ObjectRecord;
}
}

View File

@ -1,6 +1,11 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
export type FieldMetadataMap = Record<string, FieldMetadataInterface>; export type FieldMetadataMap = Record<string, FieldMetadataInterface>;
export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & { export type ObjectMetadataMapItem = Omit<ObjectMetadataInterface, 'fields'> & {
@ -33,3 +38,19 @@ export const convertObjectMetadataToMap = (
return objectMetadataMap; return objectMetadataMap;
}; };
export const getObjectMetadata = (
objectMetadataMap: Record<string, any>,
objectName: string,
): ObjectMetadataMapItem => {
const objectMetadata = objectMetadataMap[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectName}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return objectMetadata;
};

View File

@ -38,7 +38,8 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
if (error instanceof GraphqlQueryRunnerException) { if (error instanceof GraphqlQueryRunnerException) {
switch (error.code) { switch (error.code) {
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT: case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND: case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED: case GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED:
case GraphqlQueryRunnerExceptionCode.INVALID_CURSOR: case GraphqlQueryRunnerExceptionCode.INVALID_CURSOR: