Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored query result getter handlers to support recursivity #8497

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export class GraphqlQueryRunnerService {
results,
objectMetadataItemWithFieldMaps,
authContext.workspace.id,
options.objectMetadataMaps,
);

const resultWithGettersArray = Array.isArray(resultWithGetters)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';

export type QueryResultFieldValue =
| IConnection<ObjectRecord>
| { records: ObjectRecord[] }
| ObjectRecord
| ObjectRecord[];
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';

export interface QueryResultGetterHandlerInterface {
handle(result: any, workspaceId: string): Promise<any>;
handle(
objectRecord: ObjectRecord,
workspaceId: string,
): Promise<ObjectRecord>;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';

import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.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 { isQueryResultFieldValueAConnection } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/is-query-result-field-value-a-connection.guard';
import { isQueryResultFieldValueANestedRecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/is-query-result-field-value-a-nested-record-array.guard';
import { isQueryResultFieldValueARecordArray } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/is-query-result-field-value-a-record-array.guard';
import { isQueryResultFieldValueARecord } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/is-query-result-field-value-a-record.guard';
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isDefined } from 'src/utils/is-defined';

// 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 QueryResultGettersFactory {
private readonly logger = new Logger(
CompositeInputTypeDefinitionFactory.name,
);
private handlers: Map<string, QueryResultGetterHandlerInterface>;

constructor(private readonly fileService: FileService) {
Expand All @@ -30,37 +48,198 @@ export class QueryResultGettersFactory {
]);
}

async create(
result: any,
objectMetadataItem: ObjectMetadataInterface,
private async processConnection(
connection: IConnection<ObjectRecord>,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
): Promise<any> {
const handler = this.getHandler(objectMetadataItem.nameSingular);

if (result.edges) {
return {
...result,
edges: await Promise.all(
result.edges.map(async (edge: any) => ({
...edge,
node: await handler.handle(edge.node, workspaceId),
})),
): Promise<IConnection<ObjectRecord>> {
return {
...connection,
edges: await Promise.all(
connection.edges.map(async (edge: IEdge<ObjectRecord>) => ({
...edge,
node: await this.processRecord(
edge.node,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
})),
),
};
}

private async processNestedRecordArray(
result: { records: ObjectRecord[] },
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
return {
...result,
records: await Promise.all(
result.records.map(
async (record: ObjectRecord) =>
await this.processRecord(
record,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
),
),
};
}
),
};
}

if (result.records) {
return {
...result,
records: await Promise.all(
result.records.map(
async (item: any) => await handler.handle(item, workspaceId),
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];

const handler = this.getHandler(objectMetadataMapItem.nameSingular);

const relationFields = Object.keys(record)
.map(
(recordFieldName) =>
objectMetadataMapItem.fieldsByName[recordFieldName],
)
.filter(isDefined)
.filter((fieldMetadata) =>
isRelationFieldMetadataType(fieldMetadata.type),
);

const relationFieldsProcessedMap = {} as Record<
string,
QueryResultFieldValue
>;

for (const relationField of relationFields) {
const relationMetadata =
relationField.fromRelationMetadata ?? relationField.toRelationMetadata;

if (!isDefined(relationMetadata)) {
throw new Error('Relation metadata is not defined');
}

// TODO: computing this by taking the opposite of the current object metadata id
// is really less than ideal. This should be computed based on the relation metadata
// But right now it is too complex with the current structure and / or lack of utils
// around the possible combinations with relation metadata from / to + MANY_TO_ONE / ONE_TO_MANY
const relationObjectMetadataItemId =
relationMetadata.fromObjectMetadataId === objectMetadataItemId
? relationMetadata.toObjectMetadataId
: relationMetadata.fromObjectMetadataId;

const relationObjectMetadataItem =
objectMetadataMaps.byId[relationObjectMetadataItemId];

if (!isDefined(relationObjectMetadataItem)) {
throw new Error(
`Object metadata not found for id ${relationObjectMetadataItemId}`,
);
}

relationFieldsProcessedMap[relationField.name] =
await this.processQueryResultField(
record[relationField.name],
relationObjectMetadataItem.id,
objectMetadataMaps,
workspaceId,
);
}

const objectRecordProcessedWithoutRelationFields = await handler.handle(
record,
workspaceId,
);

const processedRecord = {
...objectRecordProcessedWithoutRelationFields,
...relationFieldsProcessedMap,
};

return processedRecord;
}

private async processQueryResultField(
queryResultField: QueryResultFieldValue,
objectMetadataItemId: string,
objectMetadataMaps: ObjectMetadataMaps,
workspaceId: string,
) {
if (isQueryResultFieldValueAConnection(queryResultField)) {
return await this.processConnection(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueANestedRecordArray(queryResultField)) {
return await this.processNestedRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueARecordArray(queryResultField)) {
return await this.processRecordArray(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else if (isQueryResultFieldValueARecord(queryResultField)) {
return await this.processRecord(
queryResultField,
objectMetadataItemId,
objectMetadataMaps,
workspaceId,
);
} else {
this.logger.warn(
`Query result field is not a record, connection, nested record array or record array.
This is an undetected case in query result getter that should be implemented !!`,
);

return queryResultField;
}
}

return await handler.handle(result, workspaceId);
async create(
result: QueryResultFieldValue,
objectMetadataItem: ObjectMetadataInterface,
workspaceId: string,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<any> {
return await this.processQueryResultField(
result,
objectMetadataItem.id,
objectMetadataMaps,
workspaceId,
);
}

private getHandler(objectType: string): QueryResultGetterHandlerInterface {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';

export const isQueryResultFieldValueAConnection = (
result: QueryResultFieldValue,
): result is IConnection<ObjectRecord, IEdge<ObjectRecord>> => {
return 'edges' in result && Array.isArray(result.edges);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';

export const isQueryResultFieldValueANestedRecordArray = (
result: QueryResultFieldValue,
): result is { records: ObjectRecord[] } => {
return 'records' in result && Array.isArray(result.records);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';

export const isQueryResultFieldValueARecordArray = (
result: QueryResultFieldValue,
): result is ObjectRecord[] => {
return Array.isArray(result);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';

export const isQueryResultFieldValueARecord = (
result: QueryResultFieldValue,
): result is ObjectRecord[] => {
return 'id' in result && 'createdAt' in result && 'updatedAt' in result;
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { DynamicModule, Global, ConsoleLogger, Module } from '@nestjs/common';
import { ConsoleLogger, DynamicModule, Global, Module } from '@nestjs/common';

import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
import { LOGGER_DRIVER } from 'src/engine/core-modules/logger/logger.constants';
import {
ASYNC_OPTIONS_TYPE,
ConfigurableModuleClass,
OPTIONS_TYPE,
} from 'src/engine/core-modules/logger/logger.module-definition';
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';

@Global()
@Module({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Logger } from '@nestjs/common';

import { isDefined } from 'class-validator';

/**
* A decorator function that logs the execution time of the decorated method.
*
* @returns The modified property descriptor with the execution time logging functionality.
*/
export function LogExecutionTime() {
export function LogExecutionTime(label?: string | undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The type annotation string | undefined is redundant when using optional parameter syntax ?

return function (
target: any,
propertyKey: string,
Expand All @@ -21,7 +23,11 @@ export function LogExecutionTime() {
const end = performance.now();
const executionTime = end - start;

logger.log(`Execution time: ${executionTime.toFixed(2)}ms`);
if (isDefined(label)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: isDefined() will return true for empty strings - consider using a more strict check like label?.length > 0

logger.log(`${label} execution time: ${executionTime.toFixed(2)}ms`);
} else {
logger.log(`Execution time: ${executionTime.toFixed(2)}ms`);
}

return result;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly indexMetadataService: IndexMetadataService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {
super(relationMetadataRepository);
}
Expand Down Expand Up @@ -483,6 +483,12 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
const objectMetadata =
objectMetadataMaps.byId[fieldMetadataItem.objectMetadataId];

if (!objectMetadata) {
return new NotFoundException(
`Object metadata not found for field ${fieldMetadataItem.id}`,
);
}

const fieldMetadata = objectMetadata.fieldsById[fieldMetadataItem.id];

const relationMetadata =
Expand Down
Loading
Loading