import { Injectable } from '@nestjs/common'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { GraphqlQueryBaseResolverService, GraphqlQueryResolverExecutionArgs, } from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service'; import { ObjectRecord, ObjectRecordFilter, ObjectRecordOrderBy, OrderByDirection, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-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 { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { getCursor, getPaginationInfo, } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolverService< FindManyResolverArgs, IConnection > { constructor(private readonly processAggregateHelper: ProcessAggregateHelper) { super(); } async resolve( executionArgs: GraphqlQueryResolverExecutionArgs, ): Promise> { const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = executionArgs.options; const { roleId } = executionArgs; const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, ); const aggregateQueryBuilder = queryBuilder.clone(); let appliedFilters = executionArgs.args.filter ?? ({} as ObjectRecordFilter); executionArgs.graphqlQueryParser.applyFilterToBuilder( aggregateQueryBuilder, objectMetadataItemWithFieldMaps.nameSingular, appliedFilters, ); executionArgs.graphqlQueryParser.applyDeletedAtToBuilder( aggregateQueryBuilder, appliedFilters, ); const orderByWithIdCondition = [ ...(executionArgs.args.orderBy ?? []), { id: OrderByDirection.AscNullsFirst }, ] as ObjectRecordOrderBy; const isForwardPagination = !isDefined(executionArgs.args.before); const cursor = getCursor(executionArgs.args); if (cursor) { const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, objectMetadataItemWithFieldMaps.fieldsByName, isForwardPagination, ); appliedFilters = (executionArgs.args.filter ? { and: [executionArgs.args.filter, { or: cursorArgFilter }], } : { or: cursorArgFilter }) as unknown as ObjectRecordFilter; } executionArgs.graphqlQueryParser.applyFilterToBuilder( queryBuilder, objectMetadataItemWithFieldMaps.nameSingular, appliedFilters, ); executionArgs.graphqlQueryParser.applyOrderToBuilder( queryBuilder, orderByWithIdCondition, objectMetadataItemWithFieldMaps.nameSingular, isForwardPagination, ); executionArgs.graphqlQueryParser.applyDeletedAtToBuilder( queryBuilder, appliedFilters, ); this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( { selectedAggregatedFields: executionArgs.graphqlQuerySelectedFieldsResult.aggregate, queryBuilder: aggregateQueryBuilder, }, ); const limit = executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS; const nonFormattedObjectRecords = await queryBuilder .take(limit + 1) .getMany(); const objectRecords = formatResult( nonFormattedObjectRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, ); const { hasNextPage, hasPreviousPage } = getPaginationInfo( objectRecords, limit, isForwardPagination, ); if (objectRecords.length > limit) { objectRecords.pop(); } const parentObjectRecordsAggregatedValues = await aggregateQueryBuilder.getRawOne(); if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, parentObjectRecordsAggregatedValues, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, aggregate: executionArgs.graphqlQuerySelectedFieldsResult.aggregate, limit: QUERY_MAX_RECORDS, authContext, dataSource: executionArgs.dataSource, roleId, shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); return typeORMObjectRecordsParser.createConnection({ objectRecords, objectRecordsAggregatedValues: parentObjectRecordsAggregatedValues, selectedAggregatedFields: executionArgs.graphqlQuerySelectedFieldsResult.aggregate, objectName: objectMetadataItemWithFieldMaps.nameSingular, take: limit, totalCount: parentObjectRecordsAggregatedValues?.totalCount, order: orderByWithIdCondition, hasNextPage, hasPreviousPage, }); } async validate( args: FindManyResolverArgs, _options: WorkspaceQueryRunnerOptions, ): Promise { 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, ); } } }