nestjs-paginate icon indicating copy to clipboard operation
nestjs-paginate copied to clipboard

extractVirtualProperty Fails to Retrieve Virtual Columns on Inverse Side of OneToOne Relations

Open 256Taras opened this issue 10 months ago • 1 comments
trafficstars

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 };
}

    
    

256Taras avatar Jan 08 '25 15:01 256Taras