Common api - findMany query (#15004)

Done in this PR : 
- simplify rest input parsing by removing metadata validation (for
filter and orderBy - to add in common) (1st commit)
- simplify result getter handlers signature (array of objectRecord only
for input)


closes https://github.com/twentyhq/core-team-issues/issues/1614
closes https://github.com/twentyhq/core-team-issues/issues/1615
closes https://github.com/twentyhq/core-team-issues/issues/1616
This commit is contained in:
Etienne
2025-10-13 12:48:50 +00:00
committed by GitHub
parent 4c7e8444cf
commit fc7304bc5a
67 changed files with 1291 additions and 349 deletions
@@ -4,10 +4,8 @@ import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import {
Depth,
MAX_DEPTH,
} from 'src/engine/api/rest/input-factories/depth-input.factory';
import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant';
import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@@ -3,11 +3,10 @@ import { Inject, Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { type ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
import { CommonQueryNames } from 'src/engine/api/common/types/common-query-args.type';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
@@ -30,13 +29,7 @@ import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/wo
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Injectable()
export abstract class CommonBaseQueryRunnerService<
Response extends
| ObjectRecord
| ObjectRecord[]
| IConnection<ObjectRecord, IEdge<ObjectRecord>>
| IConnection<ObjectRecord, IEdge<ObjectRecord>>[],
> {
export abstract class CommonBaseQueryRunnerService {
@Inject()
protected readonly workspaceQueryHookService: WorkspaceQueryHookService;
@Inject()
@@ -57,6 +50,8 @@ export abstract class CommonBaseQueryRunnerService<
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
@Inject()
protected readonly commonResultGettersService: CommonResultGettersService;
public async prepareQueryRunnerContext({
authContext,
@@ -107,18 +102,19 @@ export abstract class CommonBaseQueryRunnerService<
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
}: {
results: Response;
results: ObjectRecord[];
operationName: CommonQueryNames;
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}) {
const resultWithGetters = await this.queryResultGettersFactory.create(
results,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
objectMetadataMaps,
);
}): Promise<ObjectRecord[]> {
const resultWithGetters =
await this.commonResultGettersService.processQueryResult(
results,
objectMetadataItemWithFieldMaps.id,
objectMetadataMaps,
authContext.workspace.id,
);
await this.workspaceQueryHookService.executePostQueryHooks(
authContext,
@@ -0,0 +1,286 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import {
ObjectRecord,
ObjectRecordFilter,
ObjectRecordOrderBy,
OrderByDirection,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
import {
CommonQueryNames,
FindManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext: toValidateAuthContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: FindManyQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<{
records: ObjectRecord[];
aggregatedValues: Record<string, number>;
totalCount: number;
pageInfo: CommonPageInfo;
}> {
this.validate(args);
const authContext = toValidateAuthContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const {
workspaceDataSource,
repository,
roleId,
shouldBypassPermissionChecks,
} = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
});
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
const aggregateQueryBuilder = queryBuilder.clone();
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
commonQueryParser.applyFilterToBuilder(
aggregateQueryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
commonQueryParser.applyDeletedAtToBuilder(
aggregateQueryBuilder,
appliedFilters,
);
const orderByWithIdCondition = [
...(processedArgs.orderBy ?? []),
{ id: OrderByDirection.AscNullsFirst },
] as ObjectRecordOrderBy;
const isForwardPagination = !isDefined(processedArgs.before);
const cursor = getCursor(processedArgs);
if (cursor) {
const cursorArgFilter = computeCursorArgFilter(
cursor,
orderByWithIdCondition,
objectMetadataItemWithFieldMaps,
isForwardPagination,
);
appliedFilters = (processedArgs.filter
? {
and: [processedArgs.filter, { or: cursorArgFilter }],
}
: { or: cursorArgFilter }) as unknown as ObjectRecordFilter;
}
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
appliedFilters,
);
commonQueryParser.applyOrderToBuilder(
queryBuilder,
orderByWithIdCondition,
objectMetadataItemWithFieldMaps.nameSingular,
isForwardPagination,
);
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: processedArgs.selectedFieldsResult.aggregate,
queryBuilder: aggregateQueryBuilder,
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
});
const limit =
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
const columnsToSelect = buildColumnsToSelect({
select: processedArgs.selectedFieldsResult.select,
relations: processedArgs.selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const objectRecords = (await queryBuilder
.setFindOptions({
select: columnsToSelect,
})
.take(limit + 1)
.getMany()) as ObjectRecord[];
const pageInfo = getPageInfo(
objectRecords,
orderByWithIdCondition,
limit,
isForwardPagination,
);
if (objectRecords.length > limit) {
objectRecords.pop();
}
if (!isForwardPagination) {
objectRecords.reverse();
}
const parentObjectRecordsAggregatedValues =
await aggregateQueryBuilder.getRawOne();
if (processedArgs.selectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
parentObjectRecordsAggregatedValues,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: processedArgs.selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate: processedArgs.selectedFieldsResult.aggregate,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
roleId,
shouldBypassPermissionChecks,
selectedFields: processedArgs.selectedFieldsResult.select,
});
}
const enrichedRecords = await this.enrichResultsWithGettersAndHooks({
results: objectRecords,
operationName: CommonQueryNames.findMany,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
return {
records: enrichedRecords,
aggregatedValues: parentObjectRecordsAggregatedValues,
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
pageInfo,
};
}
validate(args: FindManyQueryArgs) {
if (args.first && args.last) {
throw new CommonQueryRunnerException(
'Cannot provide both first and last',
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.after) {
throw new CommonQueryRunnerException(
'Cannot provide both before and after',
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.before && args.first) {
throw new CommonQueryRunnerException(
'Cannot provide both before and first',
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.after && args.last) {
throw new CommonQueryRunnerException(
'Cannot provide both after and last',
CommonQueryRunnerExceptionCode.ARGS_CONFLICT,
);
}
if (args.first !== undefined && args.first < 0) {
throw new CommonQueryRunnerException(
'First argument must be non-negative',
CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST,
);
}
if (args.last !== undefined && args.last < 0) {
throw new CommonQueryRunnerException(
'Last argument must be non-negative',
CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST,
);
}
}
async processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: FindManyQueryArgs;
}): Promise<FindManyQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.findMany,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as FindManyQueryArgs;
return {
...hookedArgs,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
objectMetadataItemWithFieldMaps,
),
};
}
}
@@ -21,14 +21,13 @@ import {
} from 'src/engine/api/common/types/common-query-args.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@Injectable()
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService<ObjectRecord> {
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext: toValidateAuthContext,
@@ -137,23 +136,15 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
});
}
const typeORMObjectRecordsParser =
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps);
const results = typeORMObjectRecordsParser.processRecord({
objectRecord: objectRecords[0],
objectName: objectMetadataItemWithFieldMaps.nameSingular,
take: 1,
totalCount: 1,
}) as ObjectRecord;
return this.enrichResultsWithGettersAndHooks({
results,
const enrichedResults = await this.enrichResultsWithGettersAndHooks({
results: objectRecords,
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
operationName: CommonQueryNames.findOne,
});
return enrichedResults[0];
}
async processQueryArgs({
@@ -1,3 +1,7 @@
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
export const CommonQueryRunners = [CommonFindOneQueryRunnerService];
export const CommonQueryRunners = [
CommonFindOneQueryRunnerService,
CommonFindManyQueryRunnerService,
];
@@ -6,4 +6,7 @@ export enum CommonQueryRunnerExceptionCode {
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
ARGS_CONFLICT = 'ARGS_CONFLICT',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
}
@@ -16,6 +16,9 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
switch (error.code) {
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error);
case CommonQueryRunnerExceptionCode.ARGS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new UserInputError(error);
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
@@ -15,6 +15,9 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
error: CommonQueryRunnerException,
): never => {
switch (error.code) {
case CommonQueryRunnerExceptionCode.ARGS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new BadRequestException(error.message);
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
@@ -0,0 +1,155 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
// TODO: find a way to prevent conflict between handlers executing logic on object relations
// And this factory that is also executing logic on object relations
// Right now the factory will override any change made on relations by the handlers
@Injectable()
export class CommonResultGettersService {
private readonly logger = new Logger(CommonResultGettersService.name);
private handlers: Map<string, QueryResultGetterHandlerInterface>;
constructor(private readonly fileService: FileService) {
this.initializeHandlers();
}
private initializeHandlers() {
this.handlers = new Map<string, QueryResultGetterHandlerInterface>([
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
['person', new PersonQueryResultGetterHandler(this.fileService)],
[
'workspaceMember',
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
],
['note', new ActivityQueryResultGetterHandler(this.fileService)],
['task', new ActivityQueryResultGetterHandler(this.fileService)],
]);
}
private async processRecordArray(
recordArray: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
return await Promise.all(
recordArray.map(
async (record: ObjectRecord) =>
await this.processRecord(
record,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
),
);
}
private async processRecord(
record: ObjectRecord,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<ObjectRecord> {
const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId];
if (!isDefined(objectMetadataMapItem)) {
throw new Error('Object metadata map item is not defined');
}
const handler = this.getHandler(objectMetadataMapItem.nameSingular);
const relationFields = Object.keys(record)
.map(
(recordFieldName) =>
objectMetadataMapItem.fieldsById[
objectMetadataMapItem.fieldIdByName[recordFieldName]
],
)
.filter(isDefined)
.filter((fieldMetadata) =>
isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION),
);
const relationFieldsProcessedMap = {} as Record<
string,
QueryResultFieldValue
>;
for (const relationField of relationFields) {
if (!isDefined(relationField.relationTargetObjectMetadataId)) {
throw new Error('Relation target object metadata id is not defined');
}
const recordFieldValue = record[relationField.name];
if (!isDefined(recordFieldValue)) {
continue;
}
relationFieldsProcessedMap[relationField.name] =
relationField.settings?.relationType === RelationType.ONE_TO_MANY
? await this.processRecordArray(
record[relationField.name],
relationField.relationTargetObjectMetadataId,
objectMetadataMaps,
workspaceId,
)
: await this.processRecord(
record[relationField.name],
relationField.relationTargetObjectMetadataId,
objectMetadataMaps,
workspaceId,
);
}
const objectRecordProcessedWithoutRelationFields = await handler.handle(
record,
workspaceId,
);
const processedRecord = {
...objectRecordProcessedWithoutRelationFields,
...relationFieldsProcessedMap,
};
return processedRecord;
}
async processQueryResult(
queryResultField: ObjectRecord[],
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<ObjectRecord[]> {
return await this.processRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
}
private getHandler(objectType: string): QueryResultGetterHandlerInterface {
return (
this.handlers.get(objectType) || {
handle: (result: ObjectRecord): Promise<ObjectRecord> =>
Promise.resolve(result),
}
);
}
}
@@ -3,12 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers';
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@@ -23,6 +25,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
UserRoleModule,
ApiKeyModule,
WorkspacePermissionsCacheModule,
FileModule,
],
providers: [
ProcessNestedRelationsHelper,
@@ -30,6 +33,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
...CommonArgsHandlers,
ProcessAggregateHelper,
...CommonQueryRunners,
CommonResultGettersService,
],
exports: [...CommonQueryRunners],
})
@@ -0,0 +1,8 @@
import { type PageInfo } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
export type CommonPageInfo = {
hasNextPage: NonNullable<PageInfo['hasNextPage']>;
hasPreviousPage: NonNullable<PageInfo['hasPreviousPage']>;
startCursor: string | null;
endCursor: string | null;
};
@@ -1,9 +1,13 @@
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import {
type ObjectRecordFilter,
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
export enum CommonQueryNames {
findOne = 'findOne',
findMany = 'findMany',
}
export interface FindOneQueryArgs {
@@ -11,4 +15,14 @@ export interface FindOneQueryArgs {
filter?: ObjectRecordFilter;
}
export type CommonQueryArgs = FindOneQueryArgs;
export interface FindManyQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
filter?: ObjectRecordFilter;
orderBy?: ObjectRecordOrderBy;
first?: number;
last?: number;
before?: string;
after?: string;
}
export type CommonQueryArgs = FindOneQueryArgs | FindManyQueryArgs;
@@ -1,3 +1,5 @@
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
interface SelectedFields {
[key: string]: boolean | SelectedFields;
}
@@ -5,5 +7,6 @@ interface SelectedFields {
export type CommonSelectedFieldsResult = {
select: SelectedFields;
relations: SelectedFields;
aggregate: SelectedFields;
//TODO = Refacto-common - to update when rest api will handle aggregates
aggregate: Record<string, AggregationField>;
};
@@ -0,0 +1,32 @@
import {
type ObjectRecord,
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type CommonPageInfo } from 'src/engine/api/common/types/common-page-info.type';
import {
encodeCursor,
getPaginationInfo,
} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
export const getPageInfo = (
records: ObjectRecord[],
orderBy: ObjectRecordOrderBy,
limit: number,
isForwardPagination: boolean,
): CommonPageInfo => {
const { hasNextPage, hasPreviousPage } = getPaginationInfo(
records,
limit,
isForwardPagination,
);
const startCursor =
records.length > 0 ? encodeCursor(records[0], orderBy) : null;
const endCursor =
records.length > 0
? encodeCursor(records[records.length - 1], orderBy)
: null;
return { startCursor, endCursor, hasNextPage, hasPreviousPage };
};