[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 355 additions and 109 deletions

View File

@ -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',
} }

View File

@ -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;
} }

View File

@ -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}%`),

View File

@ -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}`,

View File

@ -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(

View File

@ -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,
);
}
}
} }

View File

@ -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,
); );
} }

View File

@ -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;

View File

@ -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');
}; };

View File

@ -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;
}; };

View File

@ -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,
{ {