mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-28 23:03:41 +03:00
[Flexible-schema] Add findOne and fix findMany pagination + soft-delete for graphql-query-runner (#6978)
This commit is contained in:
parent
425eb040f7
commit
1317e1c4f2
@ -8,6 +8,7 @@ export class GraphqlQueryRunnerException extends CustomException {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum GraphqlQueryRunnerExceptionCode {
|
export enum GraphqlQueryRunnerExceptionCode {
|
||||||
|
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
|
||||||
MAX_DEPTH_REACHED = 'MAX_DEPTH_REACHED',
|
MAX_DEPTH_REACHED = 'MAX_DEPTH_REACHED',
|
||||||
INVALID_CURSOR = 'INVALID_CURSOR',
|
INVALID_CURSOR = 'INVALID_CURSOR',
|
||||||
INVALID_DIRECTION = 'INVALID_DIRECTION',
|
INVALID_DIRECTION = 'INVALID_DIRECTION',
|
||||||
@ -15,4 +16,5 @@ export enum GraphqlQueryRunnerExceptionCode {
|
|||||||
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
ARGS_CONFLICT = 'ARGS_CONFLICT',
|
||||||
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',
|
||||||
}
|
}
|
||||||
|
@ -26,15 +26,19 @@ 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':
|
||||||
return this.parseAndCondition(value, isNegated);
|
Object.assign(result, this.parseAndCondition(value, isNegated));
|
||||||
|
break;
|
||||||
case 'or':
|
case 'or':
|
||||||
return this.parseOrCondition(value, isNegated);
|
orCondition = this.parseOrCondition(value, isNegated);
|
||||||
|
break;
|
||||||
case 'not':
|
case 'not':
|
||||||
return this.parse(value, !isNegated);
|
Object.assign(result, this.parse(value, !isNegated));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Object.assign(
|
Object.assign(
|
||||||
result,
|
result,
|
||||||
@ -43,6 +47,10 @@ export class GraphqlQueryFilterConditionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (orCondition) {
|
||||||
|
return orCondition.map((condition) => ({ ...result, ...condition }));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,15 @@ export class GraphqlQueryFilterOperatorParser {
|
|||||||
lt: (value: any) => LessThan(value),
|
lt: (value: any) => LessThan(value),
|
||||||
lte: (value: any) => LessThanOrEqual(value),
|
lte: (value: any) => LessThanOrEqual(value),
|
||||||
in: (value: any) => In(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}%`),
|
like: (value: string) => Like(`%${value}%`),
|
||||||
ilike: (value: string) => ILike(`%${value}%`),
|
ilike: (value: string) => ILike(`%${value}%`),
|
||||||
startsWith: (value: string) => ILike(`${value}%`),
|
startsWith: (value: string) => ILike(`${value}%`),
|
||||||
|
@ -23,7 +23,10 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
this.fieldMetadataMap = fieldMetadataMap;
|
this.fieldMetadataMap = fieldMetadataMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
parse(
|
||||||
|
orderBy: RecordOrderBy,
|
||||||
|
isForwardPagination = true,
|
||||||
|
): Record<string, FindOptionsOrderValue> {
|
||||||
return orderBy.reduce(
|
return orderBy.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
Object.entries(item).forEach(([key, value]) => {
|
Object.entries(item).forEach(([key, value]) => {
|
||||||
@ -40,11 +43,15 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
const compositeOrder = this.parseCompositeFieldForOrder(
|
const compositeOrder = this.parseCompositeFieldForOrder(
|
||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
value,
|
value,
|
||||||
|
isForwardPagination,
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(acc, compositeOrder);
|
Object.assign(acc, compositeOrder);
|
||||||
} else {
|
} else {
|
||||||
acc[key] = this.convertOrderByToFindOptionsOrder(value);
|
acc[key] = this.convertOrderByToFindOptionsOrder(
|
||||||
|
value,
|
||||||
|
isForwardPagination,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,6 +64,7 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
private parseCompositeFieldForOrder(
|
private parseCompositeFieldForOrder(
|
||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
value: any,
|
value: any,
|
||||||
|
isForwardPagination = true,
|
||||||
): Record<string, FindOptionsOrderValue> {
|
): Record<string, FindOptionsOrderValue> {
|
||||||
const compositeType = compositeTypeDefinitions.get(
|
const compositeType = compositeTypeDefinitions.get(
|
||||||
fieldMetadata.type as CompositeFieldMetadataType,
|
fieldMetadata.type as CompositeFieldMetadataType,
|
||||||
@ -87,8 +95,10 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
`Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`,
|
`Sub field order by value must be of type OrderByDirection, but got: ${subFieldValue}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
acc[fullFieldName] =
|
acc[fullFieldName] = this.convertOrderByToFindOptionsOrder(
|
||||||
this.convertOrderByToFindOptionsOrder(subFieldValue);
|
subFieldValue,
|
||||||
|
isForwardPagination,
|
||||||
|
);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
@ -98,16 +108,29 @@ export class GraphqlQueryOrderFieldParser {
|
|||||||
|
|
||||||
private convertOrderByToFindOptionsOrder(
|
private convertOrderByToFindOptionsOrder(
|
||||||
direction: OrderByDirection,
|
direction: OrderByDirection,
|
||||||
|
isForwardPagination = true,
|
||||||
): FindOptionsOrderValue {
|
): FindOptionsOrderValue {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case OrderByDirection.AscNullsFirst:
|
case OrderByDirection.AscNullsFirst:
|
||||||
return { direction: 'ASC', nulls: 'FIRST' };
|
return {
|
||||||
|
direction: isForwardPagination ? 'ASC' : 'DESC',
|
||||||
|
nulls: 'FIRST',
|
||||||
|
};
|
||||||
case OrderByDirection.AscNullsLast:
|
case OrderByDirection.AscNullsLast:
|
||||||
return { direction: 'ASC', nulls: 'LAST' };
|
return {
|
||||||
|
direction: isForwardPagination ? 'ASC' : 'DESC',
|
||||||
|
nulls: 'LAST',
|
||||||
|
};
|
||||||
case OrderByDirection.DescNullsFirst:
|
case OrderByDirection.DescNullsFirst:
|
||||||
return { direction: 'DESC', nulls: 'FIRST' };
|
return {
|
||||||
|
direction: isForwardPagination ? 'DESC' : 'ASC',
|
||||||
|
nulls: 'FIRST',
|
||||||
|
};
|
||||||
case OrderByDirection.DescNullsLast:
|
case OrderByDirection.DescNullsLast:
|
||||||
return { direction: 'DESC', nulls: 'LAST' };
|
return {
|
||||||
|
direction: isForwardPagination ? 'DESC' : 'ASC',
|
||||||
|
nulls: 'LAST',
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new GraphqlQueryRunnerException(
|
throw new GraphqlQueryRunnerException(
|
||||||
`Invalid direction: ${direction}`,
|
`Invalid direction: ${direction}`,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
FindOptionsOrderValue,
|
FindOptionsOrderValue,
|
||||||
FindOptionsWhere,
|
FindOptionsWhere,
|
||||||
|
IsNull,
|
||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
@ -32,20 +33,55 @@ export class GraphqlQueryParser {
|
|||||||
|
|
||||||
parseFilter(
|
parseFilter(
|
||||||
recordFilter: RecordFilter,
|
recordFilter: RecordFilter,
|
||||||
|
shouldAddDefaultSoftDeleteCondition = false,
|
||||||
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||||
const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
|
const graphqlQueryFilterParser = new GraphqlQueryFilterParser(
|
||||||
this.fieldMetadataMap,
|
this.fieldMetadataMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
return graphqlQueryFilterParser.parse(recordFilter);
|
const parsedFilter = graphqlQueryFilterParser.parse(recordFilter);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!shouldAddDefaultSoftDeleteCondition ||
|
||||||
|
!('deletedAt' in this.fieldMetadataMap)
|
||||||
|
) {
|
||||||
|
return parsedFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseOrder(orderBy: RecordOrderBy): Record<string, FindOptionsOrderValue> {
|
return this.addDefaultSoftDeleteCondition(parsedFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addDefaultSoftDeleteCondition(
|
||||||
|
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
||||||
|
): FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[] {
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
return filter.map((condition) =>
|
||||||
|
this.addSoftDeleteToCondition(condition),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addSoftDeleteToCondition(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSoftDeleteToCondition(
|
||||||
|
condition: FindOptionsWhere<ObjectLiteral>,
|
||||||
|
): FindOptionsWhere<ObjectLiteral> {
|
||||||
|
if (!('deletedAt' in condition)) {
|
||||||
|
return { ...condition, deletedAt: IsNull() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseOrder(
|
||||||
|
orderBy: RecordOrderBy,
|
||||||
|
isForwardPagination = true,
|
||||||
|
): Record<string, FindOptionsOrderValue> {
|
||||||
const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
|
const graphqlQueryOrderParser = new GraphqlQueryOrderParser(
|
||||||
this.fieldMetadataMap,
|
this.fieldMetadataMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
return graphqlQueryOrderParser.parse(orderBy);
|
return graphqlQueryOrderParser.parse(orderBy, isForwardPagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSelectedFields(
|
parseSelectedFields(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isDefined } from 'class-validator';
|
||||||
import graphqlFields from 'graphql-fields';
|
import graphqlFields from 'graphql-fields';
|
||||||
import { FindManyOptions, ObjectLiteral } from 'typeorm';
|
import { FindManyOptions, ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
@ -10,7 +11,10 @@ import {
|
|||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
} 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 { 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 { 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 { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
||||||
import {
|
import {
|
||||||
@ -32,7 +36,59 @@ export class GraphqlQueryRunnerService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@LogExecutionTime()
|
@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,
|
ObjectRecord extends IRecord = IRecord,
|
||||||
Filter extends RecordFilter = RecordFilter,
|
Filter extends RecordFilter = RecordFilter,
|
||||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||||
@ -43,91 +99,177 @@ export class GraphqlQueryRunnerService {
|
|||||||
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
const { authContext, objectMetadataItem, info, objectMetadataCollection } =
|
||||||
options;
|
options;
|
||||||
|
|
||||||
const repository =
|
this.validateArgsOrThrow(args);
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
|
const repository = await this.getRepository(
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedFields = graphqlFields(info);
|
|
||||||
|
|
||||||
const objectMetadataMap = convertObjectMetadataToMap(
|
const objectMetadataMap = convertObjectMetadataToMap(
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
);
|
);
|
||||||
|
const objectMetadata = this.getObjectMetadata(
|
||||||
const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular];
|
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 graphqlQueryParser = new GraphqlQueryParser(
|
const graphqlQueryParser = new GraphqlQueryParser(
|
||||||
fieldMetadataMap,
|
objectMetadata.fields,
|
||||||
objectMetadataMap,
|
objectMetadataMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
const { select, relations } = graphqlQueryParser.parseSelectedFields(
|
||||||
objectMetadataItem,
|
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
|
const cursor = this.getCursor(args);
|
||||||
? graphqlQueryParser.parseOrder(args.orderBy)
|
const limit = this.getLimit(args);
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const where = args.filter
|
this.addOrderByColumnsToSelect(order, select);
|
||||||
? 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;
|
|
||||||
|
|
||||||
const findOptions: FindManyOptions<ObjectLiteral> = {
|
const findOptions: FindManyOptions<ObjectLiteral> = {
|
||||||
where,
|
where,
|
||||||
order,
|
order,
|
||||||
select,
|
select,
|
||||||
relations,
|
relations,
|
||||||
take,
|
take: limit + 1,
|
||||||
};
|
};
|
||||||
|
const totalCount = await repository.count({ where });
|
||||||
const totalCount = await repository.count({
|
|
||||||
where,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
applyRangeFilter(where, order, cursor);
|
applyRangeFilter(where, cursor, isForwardPagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectRecords = await repository.find(findOptions);
|
const objectRecords = await repository.find(findOptions);
|
||||||
|
const { hasNextPage, hasPreviousPage } = this.getPaginationInfo(
|
||||||
|
objectRecords,
|
||||||
|
limit,
|
||||||
|
isForwardPagination,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (objectRecords.length > limit) {
|
||||||
|
objectRecords.pop();
|
||||||
|
}
|
||||||
|
|
||||||
const typeORMObjectRecordsParser =
|
const typeORMObjectRecordsParser =
|
||||||
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap);
|
||||||
|
|
||||||
return typeORMObjectRecordsParser.createConnection(
|
return typeORMObjectRecordsParser.createConnection(
|
||||||
(objectRecords as ObjectRecord[]) ?? [],
|
objectRecords as ObjectRecord[],
|
||||||
take,
|
objectMetadataItem.nameSingular,
|
||||||
|
limit,
|
||||||
totalCount,
|
totalCount,
|
||||||
order,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,19 +28,21 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
|
|
||||||
public createConnection<ObjectRecord extends IRecord = IRecord>(
|
public createConnection<ObjectRecord extends IRecord = IRecord>(
|
||||||
objectRecords: ObjectRecord[],
|
objectRecords: ObjectRecord[],
|
||||||
|
objectName: string,
|
||||||
take: number,
|
take: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||||
objectName: string,
|
hasNextPage: boolean,
|
||||||
|
hasPreviousPage: boolean,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
): IConnection<ObjectRecord> {
|
): IConnection<ObjectRecord> {
|
||||||
const edges = (objectRecords ?? []).map((objectRecord) => ({
|
const edges = (objectRecords ?? []).map((objectRecord) => ({
|
||||||
node: this.processRecord(
|
node: this.processRecord(
|
||||||
objectRecord,
|
objectRecord,
|
||||||
|
objectName,
|
||||||
take,
|
take,
|
||||||
totalCount,
|
totalCount,
|
||||||
order,
|
order,
|
||||||
objectName,
|
|
||||||
depth,
|
depth,
|
||||||
),
|
),
|
||||||
cursor: encodeCursor(objectRecord, order),
|
cursor: encodeCursor(objectRecord, order),
|
||||||
@ -49,8 +51,8 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
return {
|
return {
|
||||||
edges,
|
edges,
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: objectRecords.length === take && totalCount > take,
|
hasNextPage,
|
||||||
hasPreviousPage: false,
|
hasPreviousPage,
|
||||||
startCursor: edges[0]?.cursor,
|
startCursor: edges[0]?.cursor,
|
||||||
endCursor: edges[edges.length - 1]?.cursor,
|
endCursor: edges[edges.length - 1]?.cursor,
|
||||||
},
|
},
|
||||||
@ -58,12 +60,12 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private processRecord<T extends Record<string, any>>(
|
public processRecord<T extends Record<string, any>>(
|
||||||
objectRecord: T,
|
objectRecord: T,
|
||||||
|
objectName: string,
|
||||||
take: number,
|
take: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
order: Record<string, FindOptionsOrderValue> | undefined = {},
|
||||||
objectName: string,
|
|
||||||
depth = 0,
|
depth = 0,
|
||||||
): T {
|
): T {
|
||||||
if (depth >= CONNECTION_MAX_DEPTH) {
|
if (depth >= CONNECTION_MAX_DEPTH) {
|
||||||
@ -96,21 +98,23 @@ export class ObjectRecordsToGraphqlConnectionMapper {
|
|||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
processedObjectRecord[key] = this.createConnection(
|
processedObjectRecord[key] = this.createConnection(
|
||||||
value,
|
value,
|
||||||
|
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
||||||
|
.nameSingular,
|
||||||
take,
|
take,
|
||||||
value.length,
|
value.length,
|
||||||
order,
|
order,
|
||||||
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
false,
|
||||||
.nameSingular,
|
false,
|
||||||
depth + 1,
|
depth + 1,
|
||||||
);
|
);
|
||||||
} else if (isPlainObject(value)) {
|
} else if (isPlainObject(value)) {
|
||||||
processedObjectRecord[key] = this.processRecord(
|
processedObjectRecord[key] = this.processRecord(
|
||||||
value,
|
value,
|
||||||
|
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
||||||
|
.nameSingular,
|
||||||
take,
|
take,
|
||||||
totalCount,
|
totalCount,
|
||||||
order,
|
order,
|
||||||
getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap)
|
|
||||||
.nameSingular,
|
|
||||||
depth + 1,
|
depth + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,15 @@
|
|||||||
import {
|
import { FindOptionsWhere, LessThan, MoreThan, ObjectLiteral } from 'typeorm';
|
||||||
FindOptionsOrderValue,
|
|
||||||
FindOptionsWhere,
|
|
||||||
LessThan,
|
|
||||||
MoreThan,
|
|
||||||
ObjectLiteral,
|
|
||||||
} from 'typeorm';
|
|
||||||
|
|
||||||
export const applyRangeFilter = (
|
export const applyRangeFilter = (
|
||||||
where: FindOptionsWhere<ObjectLiteral>,
|
where: FindOptionsWhere<ObjectLiteral>,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
|
||||||
cursor: Record<string, any>,
|
cursor: Record<string, any>,
|
||||||
|
isForwardPagination = true,
|
||||||
): FindOptionsWhere<ObjectLiteral> => {
|
): FindOptionsWhere<ObjectLiteral> => {
|
||||||
if (!order) return where;
|
Object.entries(cursor ?? {}).forEach(([key, value]) => {
|
||||||
|
if (key === 'id') {
|
||||||
const orderEntries = Object.entries(order);
|
|
||||||
|
|
||||||
orderEntries.forEach(([column, order], index) => {
|
|
||||||
if (typeof order !== 'object' || !('direction' in order)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
where[column] =
|
where[key] = isForwardPagination ? MoreThan(value) : LessThan(value);
|
||||||
order.direction === 'ASC'
|
|
||||||
? MoreThan(cursor[index])
|
|
||||||
: LessThan(cursor[index]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return where;
|
return where;
|
||||||
|
@ -7,7 +7,11 @@ import {
|
|||||||
GraphqlQueryRunnerExceptionCode,
|
GraphqlQueryRunnerExceptionCode,
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
|
|
||||||
export const decodeCursor = (cursor: string): Record<string, any> => {
|
export interface CursorData {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodeCursor = (cursor: string): CursorData => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
return JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -22,13 +26,16 @@ export const encodeCursor = <ObjectRecord extends IRecord = IRecord>(
|
|||||||
objectRecord: ObjectRecord,
|
objectRecord: ObjectRecord,
|
||||||
order: Record<string, FindOptionsOrderValue> | undefined,
|
order: Record<string, FindOptionsOrderValue> | undefined,
|
||||||
): string => {
|
): string => {
|
||||||
const cursor = {};
|
const orderByValues: Record<string, any> = {};
|
||||||
|
|
||||||
Object.keys(order ?? []).forEach((key) => {
|
Object.keys(order ?? {}).forEach((key) => {
|
||||||
cursor[key] = objectRecord[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');
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
GraphqlQueryRunnerException,
|
||||||
|
GraphqlQueryRunnerExceptionCode,
|
||||||
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
import {
|
import {
|
||||||
WorkspaceQueryRunnerException,
|
WorkspaceQueryRunnerException,
|
||||||
WorkspaceQueryRunnerExceptionCode,
|
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;
|
throw error;
|
||||||
};
|
};
|
||||||
|
@ -122,10 +122,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||||
|
|
||||||
if (isQueryRunnerTwentyORMEnabled) {
|
if (isQueryRunnerTwentyORMEnabled) {
|
||||||
return this.graphqlQueryRunnerService.findManyWithTwentyOrm(
|
return this.graphqlQueryRunnerService.findMany(computedArgs, options);
|
||||||
computedArgs,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||||
@ -169,6 +166,12 @@ export class WorkspaceQueryRunnerService {
|
|||||||
}
|
}
|
||||||
const { authContext, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
|
const isQueryRunnerTwentyORMEnabled =
|
||||||
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
|
||||||
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
const hookedArgs =
|
const hookedArgs =
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
authContext,
|
authContext,
|
||||||
@ -183,6 +186,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
ResolverArgsType.FindOne,
|
ResolverArgsType.FindOne,
|
||||||
)) as FindOneResolverArgs<Filter>;
|
)) as FindOneResolverArgs<Filter>;
|
||||||
|
|
||||||
|
if (isQueryRunnerTwentyORMEnabled) {
|
||||||
|
return this.graphqlQueryRunnerService.findOne(computedArgs, options);
|
||||||
|
}
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findOne(
|
const query = await this.workspaceQueryBuilderFactory.findOne(
|
||||||
computedArgs,
|
computedArgs,
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user