nestjs-paginate
nestjs-paginate copied to clipboard
extractVirtualProperty Fails to Retrieve Virtual Columns on Inverse Side of OneToOne Relations
Problem:
The extractVirtualProperty function does not correctly retrieve metadata for virtual properties defined on the inverse side of a OneToOne relationship. Specifically, when accessing a virtual column like fullName through the inverse side (User.profile), the function returns undefined instead of the expected ColumnMetadata
Entities Example:
export class User extends BaseEntity {
@OneToOne(() => Profile, (profile) => profile.user, { cascade: ['insert', 'update'] })
profile: Profile;
}
export class Profile extends BaseEntity {
@Column({ type: 'varchar', length: 255, nullable: true })
firstName: string;
@Column({ type: 'varchar', length: 255, nullable: true })
lastName: string;
@VirtualColumn({
query: (alias) => `CONCAT(${alias}.firstName, ' ', ${alias}.lastName)`,
})
fullName: string;
}
Original Function with Issue:
export function extractVirtualProperty(
qb: SelectQueryBuilder<unknown>,
columnProperties: ColumnProperties
): Partial<ColumnMetadata> {
const metadata = columnProperties.propertyPath
? qb?.expressionMap?.mainAlias?.metadata?.findColumnWithPropertyPath(columnProperties.propertyPath)
?.referencedColumn?.entityMetadata // on relation
: qb?.expressionMap?.mainAlias?.metadata
return (
metadata?.columns?.find((column) => column.propertyName === columnProperties.propertyName) || {
isVirtualProperty: false,
query: undefined,
}
)
}
Issue Details:
When attempting to extract metadata for the virtual property fullName on the Profile entity via the inverse side (User.profile), the extractVirtualProperty function returns undefined. This occurs because findColumnWithPropertyPath("profile") on the User entity does not locate the join column (userId) since it resides on the Profile side. Consequently, the metadata for fullName is not retrieved, and the function incorrectly determines that fullName is not a virtual property.
Steps to Reproduce:
Setup Entities:
Define User and Profile entities with a OneToOne relationship, where Profile is the owning side containing the foreign key userId and a virtual column fullName.
Implement extractVirtualProperty:
Use the provided original implementation of extractVirtualProperty.
Execute Function:
Call extractVirtualProperty with propertyPath: 'profile' and propertyName: 'fullName' while querying the User entity.
Observe Result:
The function returns { isVirtualProperty: false, query: undefined } instead of the expected ColumnMetadata for fullName.
Expected Behavior:
The extractVirtualProperty function should correctly identify and return the metadata for the virtual column fullName on the Profile entity, even when accessed via the inverse side (User.profile) of the OneToOne relationship.
Actual Behavior:
The function returns undefined for the virtual column fullName when accessed via the inverse side, incorrectly indicating that fullName is not a virtual property.
Proposed Solution:
Modify the extractVirtualProperty function to handle both owning and inverse sides of relationships. Specifically, when findColumnWithPropertyPath does not locate a join column (i.e., returns undefined), the function should search the relation metadata to access the inverse entity's metadata and then locate the virtual column within that context.
Updated Function Implementation:
export function extractVirtualProperty(
qb: SelectQueryBuilder<unknown>,
columnProperties: ColumnProperties
): Partial<ColumnMetadata> {
const mainAliasMetadata = qb?.expressionMap?.mainAlias?.metadata;
if (!mainAliasMetadata) {
return { isVirtualProperty: false, query: undefined };
}
let targetMetadata: EntityMetadata | undefined;
// If propertyPath is not provided, use the main entity's metadata
if (!columnProperties.propertyPath) {
targetMetadata = mainAliasMetadata;
} else {
// Attempt to find the column or join column using TypeORM's method
const foundColOrJoin = mainAliasMetadata.findColumnWithPropertyPath(columnProperties.propertyPath);
if (foundColOrJoin?.referencedColumn) {
// Owning side: use the referenced entity's metadata
targetMetadata = foundColOrJoin.referencedColumn.entityMetadata;
} else {
// Inverse side: find the relation and use the inverse entity's metadata
const relation = mainAliasMetadata.relations.find(
(rel) => rel.propertyPath === columnProperties.propertyPath
);
if (relation) {
targetMetadata = relation.inverseEntityMetadata;
}
}
}
// If metadata is still undefined, return default
if (!targetMetadata) {
return { isVirtualProperty: false, query: undefined };
}
// Search for the column (including virtual columns) by propertyName
const foundColumn = targetMetadata.columns.find(
(column) => column.propertyName === columnProperties.propertyName
);
return foundColumn || { isVirtualProperty: false, query: undefined };
}