-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
b9608cd
025a135
c7efc99
6f40e39
130cd55
822a645
add4a58
a78b297
ea89cc3
d83400b
1f6663a
c298be7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,21 @@ | ||
import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; | ||
|
||
import { FileService } from 'src/engine/core-modules/file/services/file.service'; | ||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | ||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; | ||
|
||
export class PersonQueryResultGetterHandler | ||
implements QueryResultGetterHandlerInterface | ||
{ | ||
constructor(private readonly fileService: FileService) {} | ||
|
||
@LogExecutionTime('PersonQueryResultGetterHandler.handle') | ||
async handle( | ||
person: PersonWorkspaceEntity, | ||
workspaceId: string, | ||
): Promise<PersonWorkspaceEntity> { | ||
console.log('PersonQueryResultGetterHandler.handle'); | ||
|
||
if (!person.id || !person?.avatarUrl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Optional chaining (?.) is redundant here since person.id is already checked |
||
return person; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
import { Record as ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/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,14 +1,35 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
|
||
import { Record as ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||
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 { | ||
isPossibleFieldValueAConnection, | ||
isPossibleFieldValueANestedRecordArray, | ||
isPossibleFieldValueARecordArray, | ||
} from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection'; | ||
import { FileService } from 'src/engine/core-modules/file/services/file.service'; | ||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | ||
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||
import { isDefined } from 'src/utils/is-defined'; | ||
|
||
export type PossibleQueryResultFieldValue = | ||
| IConnection<ObjectRecord> | ||
| { records: ObjectRecord[] } | ||
| ObjectRecord | ||
| ObjectRecord[]; | ||
|
||
// 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 handlers: Map<string, QueryResultGetterHandlerInterface>; | ||
|
@@ -30,37 +51,189 @@ export class QueryResultGettersFactory { | |
]); | ||
} | ||
|
||
async create( | ||
result: any, | ||
objectMetadataItem: ObjectMetadataInterface, | ||
async processConnection( | ||
connection: IConnection<ObjectRecord>, | ||
objectMetadataItemId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
workspaceId: string, | ||
): Promise<any> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: processConnection returns Promise while having strongly typed input parameters. Consider using concrete return type. |
||
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), | ||
})), | ||
return { | ||
...connection, | ||
edges: await Promise.all( | ||
connection.edges.map(async (edge: IEdge<ObjectRecord>) => ({ | ||
...edge, | ||
node: await this.processRecord( | ||
edge.node, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
), | ||
})), | ||
), | ||
}; | ||
} | ||
|
||
async processNestedRecordArray( | ||
result: { records: ObjectRecord[] }, | ||
objectMetadataItemId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
workspaceId: string, | ||
) { | ||
return { | ||
...result, | ||
records: await Promise.all( | ||
result.records.map( | ||
async (record: ObjectRecord) => | ||
await this.processRecord( | ||
record, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
), | ||
), | ||
}; | ||
} | ||
), | ||
}; | ||
} | ||
|
||
if (result.records) { | ||
return { | ||
...result, | ||
records: await Promise.all( | ||
result.records.map( | ||
async (item: any) => await handler.handle(item, workspaceId), | ||
async processRecordArray( | ||
recordArray: ObjectRecord[], | ||
objectMetadataItemId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
workspaceId: string, | ||
) { | ||
return await Promise.all( | ||
recordArray.map( | ||
async (record: ObjectRecord) => | ||
await this.processRecord( | ||
record, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
), | ||
), | ||
}; | ||
), | ||
); | ||
} | ||
|
||
async processRecord( | ||
record: ObjectRecord, | ||
objectMetadataItemId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
workspaceId: string, | ||
): Promise<ObjectRecord> { | ||
const objectMetadataMapItem = objectMetadataMap[objectMetadataItemId]; | ||
|
||
const handler = this.getHandler(objectMetadataMapItem.nameSingular); | ||
|
||
const relationFields = Object.keys(record) | ||
.map((recordFieldName) => objectMetadataMapItem.fields[recordFieldName]) | ||
.filter(isDefined) | ||
.filter((fieldMetadata) => | ||
isRelationFieldMetadataType(fieldMetadata.type), | ||
); | ||
|
||
const relationFieldsProcessedMap = {} as Record< | ||
string, | ||
PossibleQueryResultFieldValue | ||
>; | ||
|
||
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 = | ||
objectMetadataMap[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, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} | ||
|
||
const objectRecordProcessedWithoutRelationFields = await handler.handle( | ||
record, | ||
workspaceId, | ||
); | ||
|
||
const newRecord = { | ||
...objectRecordProcessedWithoutRelationFields, | ||
...relationFieldsProcessedMap, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Handler results are being overridden by relationFieldsProcessedMap spread. This could cause loss of handler-specific processing for relation fields. |
||
|
||
return newRecord; | ||
} | ||
|
||
async processQueryResultField( | ||
queryResultField: PossibleQueryResultFieldValue, | ||
objectMetadataItemId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
workspaceId: string, | ||
) { | ||
if (isPossibleFieldValueAConnection(queryResultField)) { | ||
return await this.processConnection( | ||
queryResultField, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} else if (isPossibleFieldValueANestedRecordArray(queryResultField)) { | ||
return await this.processNestedRecordArray( | ||
queryResultField, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} else if (isPossibleFieldValueARecordArray(queryResultField)) { | ||
return await this.processRecordArray( | ||
queryResultField, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} else { | ||
return await this.processRecord( | ||
queryResultField, | ||
objectMetadataItemId, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} | ||
} | ||
|
||
return await handler.handle(result, workspaceId); | ||
@LogExecutionTime('QueryResultGettersFactory.create') | ||
async create( | ||
result: PossibleQueryResultFieldValue, | ||
objectMetadataItem: ObjectMetadataInterface, | ||
workspaceId: string, | ||
objectMetadataMap: ObjectMetadataMap, | ||
): Promise<any> { | ||
return await this.processQueryResultField( | ||
result, | ||
objectMetadataItem.id, | ||
objectMetadataMap, | ||
workspaceId, | ||
); | ||
} | ||
|
||
private getHandler(objectType: string): QueryResultGetterHandlerInterface { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { isDefined } from 'class-validator'; | ||
|
||
import { Record as ObjectRecord } 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 { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; | ||
|
||
import { PossibleQueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; | ||
|
||
export const isPossibleFieldValueAConnection = ( | ||
result: PossibleQueryResultFieldValue, | ||
): result is IConnection<ObjectRecord, IEdge<ObjectRecord>> => { | ||
return isDefined((result as any).edges); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Using (result as any).edges bypasses type safety. Consider checking if result is an object first with typeof result === 'object' && result !== null |
||
}; | ||
|
||
export const isPossibleFieldValueANestedRecordArray = ( | ||
result: PossibleQueryResultFieldValue, | ||
): result is { records: ObjectRecord[] } => { | ||
return 'records' in result && Array.isArray(result.records); | ||
}; | ||
|
||
export const isPossibleFieldValueARecordArray = ( | ||
result: PossibleQueryResultFieldValue, | ||
): result is ObjectRecord[] => { | ||
return Array.isArray(result); | ||
}; |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The type annotation |
||
return function ( | ||
target: any, | ||
propertyKey: string, | ||
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
logger.log(`${label} execution time: ${executionTime.toFixed(2)}ms`); | ||
} else { | ||
logger.log(`Execution time: ${executionTime.toFixed(2)}ms`); | ||
} | ||
|
||
return result; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider removing this console.log or replacing with proper logger since execution time is already being logged via decorator