diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index d7ea0e9792..2d43ac86fe 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -8,6 +8,7 @@ export class GraphqlQueryRunnerException extends CustomException { } export enum GraphqlQueryRunnerExceptionCode { + INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT', MAX_DEPTH_REACHED = 'MAX_DEPTH_REACHED', INVALID_CURSOR = 'INVALID_CURSOR', INVALID_DIRECTION = 'INVALID_DIRECTION', @@ -15,4 +16,5 @@ export enum GraphqlQueryRunnerExceptionCode { ARGS_CONFLICT = 'ARGS_CONFLICT', FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', + RECORD_NOT_FOUND = 'RECORD_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index 82421ed74e..2a86d032ea 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -26,15 +26,19 @@ export class GraphqlQueryFilterConditionParser { } const result: FindOptionsWhere = {}; + let orCondition: FindOptionsWhere[] | null = null; for (const [key, value] of Object.entries(conditions)) { switch (key) { case 'and': - return this.parseAndCondition(value, isNegated); + Object.assign(result, this.parseAndCondition(value, isNegated)); + break; case 'or': - return this.parseOrCondition(value, isNegated); + orCondition = this.parseOrCondition(value, isNegated); + break; case 'not': - return this.parse(value, !isNegated); + Object.assign(result, this.parse(value, !isNegated)); + break; default: Object.assign( result, @@ -43,6 +47,10 @@ export class GraphqlQueryFilterConditionParser { } } + if (orCondition) { + return orCondition.map((condition) => ({ ...result, ...condition })); + } + return result; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts index 3c831e4245..8deb046644 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts @@ -28,7 +28,15 @@ export class GraphqlQueryFilterOperatorParser { lt: (value: any) => LessThan(value), lte: (value: any) => LessThanOrEqual(value), in: (value: any) => In(value), - is: (value: any) => (value === 'NULL' ? IsNull() : value), + is: (value: any) => { + if (value === 'NULL') { + return IsNull(); + } else if (value === 'NOT_NULL') { + return Not(IsNull()); + } else { + return value; + } + }, like: (value: string) => Like(`%${value}%`), ilike: (value: string) => ILike(`%${value}%`), startsWith: (value: string) => ILike(`${value}%`), diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index d64a4b3e89..9b4a0abdc0 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -23,7 +23,10 @@ export class GraphqlQueryOrderFieldParser { this.fieldMetadataMap = fieldMetadataMap; } - parse(orderBy: RecordOrderBy): Record { + parse( + orderBy: RecordOrderBy, + isForwardPagination = true, + ): Record { return orderBy.reduce( (acc, item) => { Object.entries(item).forEach(([key, value]) => { @@ -40,11 +43,15 @@ export class GraphqlQueryOrderFieldParser { const compositeOrder = this.parseCompositeFieldForOrder( fieldMetadata, value, + isForwardPagination, ); Object.assign(acc, compositeOrder); } else { - acc[key] = this.convertOrderByToFindOptionsOrder(value); + acc[key] = this.convertOrderByToFindOptionsOrder( + value, + isForwardPagination, + ); } }); @@ -57,6 +64,7 @@ export class GraphqlQueryOrderFieldParser { private parseCompositeFieldForOrder( fieldMetadata: FieldMetadataInterface, value: any, + isForwardPagination = true, ): Record { const compositeType = compositeTypeDefinitions.get( fieldMetadata.type as CompositeFieldMetadataType, @@ -87,8 +95,10 @@ export class GraphqlQueryOrderFieldParser { `Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`, ); } - acc[fullFieldName] = - this.convertOrderByToFindOptionsOrder(subFieldValue); + acc[fullFieldName] = this.convertOrderByToFindOptionsOrder( + subFieldValue, + isForwardPagination, + ); return acc; }, @@ -98,16 +108,29 @@ export class GraphqlQueryOrderFieldParser { private convertOrderByToFindOptionsOrder( direction: OrderByDirection, + isForwardPagination = true, ): FindOptionsOrderValue { switch (direction) { case OrderByDirection.AscNullsFirst: - return { direction: 'ASC', nulls: 'FIRST' }; + return { + direction: isForwardPagination ? 'ASC' : 'DESC', + nulls: 'FIRST', + }; case OrderByDirection.AscNullsLast: - return { direction: 'ASC', nulls: 'LAST' }; + return { + direction: isForwardPagination ? 'ASC' : 'DESC', + nulls: 'LAST', + }; case OrderByDirection.DescNullsFirst: - return { direction: 'DESC', nulls: 'FIRST' }; + return { + direction: isForwardPagination ? 'DESC' : 'ASC', + nulls: 'FIRST', + }; case OrderByDirection.DescNullsLast: - return { direction: 'DESC', nulls: 'LAST' }; + return { + direction: isForwardPagination ? 'DESC' : 'ASC', + nulls: 'LAST', + }; default: throw new GraphqlQueryRunnerException( `Invalid direction: ${direction}`, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index ed52b4ef4c..1b1217e906 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -1,6 +1,7 @@ import { FindOptionsOrderValue, FindOptionsWhere, + IsNull, ObjectLiteral, } from 'typeorm'; @@ -32,20 +33,55 @@ export class GraphqlQueryParser { parseFilter( recordFilter: RecordFilter, + shouldAddDefaultSoftDeleteCondition = false, ): FindOptionsWhere | FindOptionsWhere[] { const graphqlQueryFilterParser = new GraphqlQueryFilterParser( this.fieldMetadataMap, ); - return graphqlQueryFilterParser.parse(recordFilter); + const parsedFilter = graphqlQueryFilterParser.parse(recordFilter); + + if ( + !shouldAddDefaultSoftDeleteCondition || + !('deletedAt' in this.fieldMetadataMap) + ) { + return parsedFilter; + } + + return this.addDefaultSoftDeleteCondition(parsedFilter); } - parseOrder(orderBy: RecordOrderBy): Record { + private addDefaultSoftDeleteCondition( + filter: FindOptionsWhere | FindOptionsWhere[], + ): FindOptionsWhere | FindOptionsWhere[] { + if (Array.isArray(filter)) { + return filter.map((condition) => + this.addSoftDeleteToCondition(condition), + ); + } + + return this.addSoftDeleteToCondition(filter); + } + + private addSoftDeleteToCondition( + condition: FindOptionsWhere, + ): FindOptionsWhere { + if (!('deletedAt' in condition)) { + return { ...condition, deletedAt: IsNull() }; + } + + return condition; + } + + parseOrder( + orderBy: RecordOrderBy, + isForwardPagination = true, + ): Record { const graphqlQueryOrderParser = new GraphqlQueryOrderParser( this.fieldMetadataMap, ); - return graphqlQueryOrderParser.parse(orderBy); + return graphqlQueryOrderParser.parse(orderBy, isForwardPagination); } parseSelectedFields( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 49c3b95d75..895110b636 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'class-validator'; import graphqlFields from 'graphql-fields'; import { FindManyOptions, ObjectLiteral } from 'typeorm'; @@ -10,7 +11,10 @@ import { } 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 { + FindManyResolverArgs, + FindOneResolverArgs, +} 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 { @@ -32,7 +36,59 @@ export class GraphqlQueryRunnerService { ) {} @LogExecutionTime() - async findManyWithTwentyOrm< + async findOne< + ObjectRecord extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + >( + args: FindOneResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise { + const { authContext, objectMetadataItem, info, objectMetadataCollection } = + 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 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() + async findMany< ObjectRecord extends IRecord = IRecord, Filter extends RecordFilter = RecordFilter, OrderBy extends RecordOrderBy = RecordOrderBy, @@ -43,91 +99,177 @@ export class GraphqlQueryRunnerService { const { authContext, objectMetadataItem, info, objectMetadataCollection } = options; - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - authContext.workspace.id, - objectMetadataItem.nameSingular, - ); - - const selectedFields = graphqlFields(info); + this.validateArgsOrThrow(args); + const repository = await this.getRepository( + authContext.workspace.id, + objectMetadataItem.nameSingular, + ); const objectMetadataMap = convertObjectMetadataToMap( objectMetadataCollection, ); - - const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular]; - - if (!objectMetadata) { - throw new GraphqlQueryRunnerException( - `Object metadata not found for ${objectMetadataItem.nameSingular}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - const fieldMetadataMap = objectMetadata.fields; - + const objectMetadata = this.getObjectMetadata( + objectMetadataMap, + objectMetadataItem.nameSingular, + ); const graphqlQueryParser = new GraphqlQueryParser( - fieldMetadataMap, + objectMetadata.fields, objectMetadataMap, ); const { select, relations } = graphqlQueryParser.parseSelectedFields( objectMetadataItem, - selectedFields, + graphqlFields(info), + ); + const isForwardPagination = !isDefined(args.before); + const order = graphqlQueryParser.parseOrder( + args.orderBy ?? [], + isForwardPagination, + ); + const where = graphqlQueryParser.parseFilter( + args.filter ?? ({} as Filter), + true, ); - const order = args.orderBy - ? graphqlQueryParser.parseOrder(args.orderBy) - : undefined; + const cursor = this.getCursor(args); + const limit = this.getLimit(args); - const where = args.filter - ? graphqlQueryParser.parseFilter(args.filter) - : {}; - - let cursor: Record | undefined; - - if (args.after) { - cursor = decodeCursor(args.after); - } else if (args.before) { - cursor = decodeCursor(args.before); - } - - if (args.first && args.last) { - throw new GraphqlQueryRunnerException( - 'Cannot provide both first and last', - GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT, - ); - } - - const take = args.first ?? args.last ?? QUERY_MAX_RECORDS; + this.addOrderByColumnsToSelect(order, select); const findOptions: FindManyOptions = { where, order, select, relations, - take, + take: limit + 1, }; - - const totalCount = await repository.count({ - where, - }); + const totalCount = await repository.count({ where }); if (cursor) { - applyRangeFilter(where, order, 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[]) ?? [], - take, + objectRecords as ObjectRecord[], + objectMetadataItem.nameSingular, + limit, totalCount, order, - objectMetadataItem.nameSingular, + hasNextPage, + hasPreviousPage, ); } + + private async getRepository(workspaceId: string, objectName: string) { + return this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + ); + } + + private getObjectMetadata( + objectMetadataMap: Record, + 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, + ): Record | undefined { + if (args.after) return decodeCursor(args.after); + if (args.before) return decodeCursor(args.before); + + return undefined; + } + + private getLimit(args: FindManyResolverArgs): number { + return args.first ?? args.last ?? QUERY_MAX_RECORDS; + } + + private addOrderByColumnsToSelect( + order: Record, + select: Record, + ) { + 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) { + 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, + ); + } + } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts index eecf0c0e1f..718add79e1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts @@ -28,19 +28,21 @@ export class ObjectRecordsToGraphqlConnectionMapper { public createConnection( objectRecords: ObjectRecord[], + objectName: string, take: number, totalCount: number, order: Record | undefined, - objectName: string, + hasNextPage: boolean, + hasPreviousPage: boolean, depth = 0, ): IConnection { const edges = (objectRecords ?? []).map((objectRecord) => ({ node: this.processRecord( objectRecord, + objectName, take, totalCount, order, - objectName, depth, ), cursor: encodeCursor(objectRecord, order), @@ -49,8 +51,8 @@ export class ObjectRecordsToGraphqlConnectionMapper { return { edges, pageInfo: { - hasNextPage: objectRecords.length === take && totalCount > take, - hasPreviousPage: false, + hasNextPage, + hasPreviousPage, startCursor: edges[0]?.cursor, endCursor: edges[edges.length - 1]?.cursor, }, @@ -58,12 +60,12 @@ export class ObjectRecordsToGraphqlConnectionMapper { }; } - private processRecord>( + public processRecord>( objectRecord: T, + objectName: string, take: number, totalCount: number, - order: Record | undefined, - objectName: string, + order: Record | undefined = {}, depth = 0, ): T { if (depth >= CONNECTION_MAX_DEPTH) { @@ -96,21 +98,23 @@ export class ObjectRecordsToGraphqlConnectionMapper { if (Array.isArray(value)) { processedObjectRecord[key] = this.createConnection( value, + getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) + .nameSingular, take, value.length, order, - getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) - .nameSingular, + false, + false, depth + 1, ); } else if (isPlainObject(value)) { processedObjectRecord[key] = this.processRecord( value, + getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) + .nameSingular, take, totalCount, order, - getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) - .nameSingular, depth + 1, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts index 0af8d935ca..3a536c68c9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util.ts @@ -1,28 +1,15 @@ -import { - FindOptionsOrderValue, - FindOptionsWhere, - LessThan, - MoreThan, - ObjectLiteral, -} from 'typeorm'; +import { FindOptionsWhere, LessThan, MoreThan, ObjectLiteral } from 'typeorm'; export const applyRangeFilter = ( where: FindOptionsWhere, - order: Record | undefined, cursor: Record, + isForwardPagination = true, ): FindOptionsWhere => { - if (!order) return where; - - const orderEntries = Object.entries(order); - - orderEntries.forEach(([column, order], index) => { - if (typeof order !== 'object' || !('direction' in order)) { + Object.entries(cursor ?? {}).forEach(([key, value]) => { + if (key === 'id') { return; } - where[column] = - order.direction === 'ASC' - ? MoreThan(cursor[index]) - : LessThan(cursor[index]); + where[key] = isForwardPagination ? MoreThan(value) : LessThan(value); }); return where; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts index 5adc036e4a..5a556850fd 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -7,7 +7,11 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -export const decodeCursor = (cursor: string): Record => { +export interface CursorData { + [key: string]: any; +} + +export const decodeCursor = (cursor: string): CursorData => { try { return JSON.parse(Buffer.from(cursor, 'base64').toString()); } catch (err) { @@ -22,13 +26,16 @@ export const encodeCursor = ( objectRecord: ObjectRecord, order: Record | undefined, ): string => { - const cursor = {}; + const orderByValues: Record = {}; - Object.keys(order ?? []).forEach((key) => { - cursor[key] = objectRecord[key]; + Object.keys(order ?? {}).forEach((key) => { + orderByValues[key] = objectRecord[key]; }); - cursor['id'] = objectRecord.id; + const cursorData: CursorData = { + ...orderByValues, + id: objectRecord.id, + }; - return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64'); + return Buffer.from(JSON.stringify(cursorData)).toString('base64'); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts index 2a52c52730..3c416b9dbb 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -1,3 +1,7 @@ +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { WorkspaceQueryRunnerException, WorkspaceQueryRunnerExceptionCode, @@ -32,5 +36,23 @@ export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( } } + if (error instanceof GraphqlQueryRunnerException) { + switch (error.code) { + case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND: + case GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED: + case GraphqlQueryRunnerExceptionCode.INVALID_CURSOR: + case GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION: + case GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR: + case GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT: + case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND: + throw new UserInputError(error.message); + case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND: + throw new NotFoundError(error.message); + default: + throw new InternalServerError(error.message); + } + } + throw error; }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index 827dd28939..679f636ebe 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -122,10 +122,7 @@ export class WorkspaceQueryRunnerService { )) as FindManyResolverArgs; if (isQueryRunnerTwentyORMEnabled) { - return this.graphqlQueryRunnerService.findManyWithTwentyOrm( - computedArgs, - options, - ); + return this.graphqlQueryRunnerService.findMany(computedArgs, options); } const query = await this.workspaceQueryBuilderFactory.findMany( @@ -169,6 +166,12 @@ export class WorkspaceQueryRunnerService { } const { authContext, objectMetadataItem } = options; + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + authContext.workspace.id, + ); + const hookedArgs = await this.workspaceQueryHookService.executePreQueryHooks( authContext, @@ -183,6 +186,10 @@ export class WorkspaceQueryRunnerService { ResolverArgsType.FindOne, )) as FindOneResolverArgs; + if (isQueryRunnerTwentyORMEnabled) { + return this.graphqlQueryRunnerService.findOne(computedArgs, options); + } + const query = await this.workspaceQueryBuilderFactory.findOne( computedArgs, {