Files
twenty/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts

221 lines
7.3 KiB
TypeScript

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<ObjectRecord>
> {
constructor(private readonly processAggregateHelper: ProcessAggregateHelper) {
super();
}
async resolve(
executionArgs: GraphqlQueryResolverExecutionArgs<FindManyResolverArgs>,
): Promise<IConnection<ObjectRecord>> {
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<ObjectRecord[]>(
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<Filter extends ObjectRecordFilter>(
args: FindManyResolverArgs<Filter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {
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,
);
}
}
}