[Flexible-schema] Add findOne and fix findMany pagination + soft-delete for graphql-query-runner (#6978)

This commit is contained in:
Weiko
2024-09-11 11:29:56 +02:00
committed by GitHub
parent 425eb040f7
commit 1317e1c4f2
11 changed files with 355 additions and 109 deletions

View File

@ -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<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<ObjectRecord | undefined> {
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<string, any> | 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<ObjectLiteral> = {
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<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,
);
}
}
}