Refactor graphql query runner + fix nested or (#6986)
This commit is contained in:
@ -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',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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', () => {
|
||||||
|
|||||||
@ -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],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user