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:
+2
-4
@@ -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';
|
||||
|
||||
+14
-18
@@ -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,
|
||||
|
||||
+286
@@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-14
@@ -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({
|
||||
|
||||
+5
-1
@@ -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,
|
||||
];
|
||||
|
||||
+3
@@ -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',
|
||||
}
|
||||
|
||||
+3
@@ -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:
|
||||
|
||||
+3
@@ -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:
|
||||
|
||||
+155
@@ -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;
|
||||
|
||||
+4
-1
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user