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

TypeOrm 3: search is broken in array of enums (postgres) #8765

Closed
TrejGun opened this issue Mar 19, 2022 · 15 comments
Closed

TypeOrm 3: search is broken in array of enums (postgres) #8765

TrejGun opened this issue Mar 19, 2022 · 15 comments
Labels

Comments

@TrejGun
Copy link

TrejGun commented Mar 19, 2022

Issue Description

Search by array property is broken

Expected Behavior

Same behavior as in v2

Actual Behavior

throws type error

Steps to Reproduce

export enum AdminRole {
  ADMIN = "ADMIN",
  MODERATOR = "MODERATOR",
}

@Entity({ schema: ns, name: "admin" })
export class AdminEntity extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column({ type: "varchar" })
  public displayName: string;

  @Column({
    type: "enum",
    enum: AdminRole,
    array: true,
  })
  public adminRoles: Array<AdminRole>;
}

v2 worked fine

  public async autocomplete(): Promise<Array<AdminEntity>> {
    return this.adminEntityRepository.find({
      where: { adminRoles: [AdminRole.ADMIN] },
      select: ["id", "displayName"],
    });
  }

v3 shows type error

  public async autocomplete(): Promise<Array<AdminEntity>> {
    return this.adminEntityRepository.find({
      where: { adminRoles: [AdminRole.ADMIN] },
      select: {
        id: true,
        displayName: true,
      },
    });
  }
TS2322: Type '{ adminRoles: AdminRole[]; }' is not assignable to type 'FindOptionsWhere<AdminEntity> | FindOptionsWhere<AdminEntity>[] | undefined'.   Types of property 'adminRoles' are incompatible.     Type 'AdminRole[]' is not assignable to type 'AdminRole | FindOperator<AdminRole.ADMIN> | FindOperator<AdminRole.MODERATOR> | undefined'.

I tried to fix with

  public async autocomplete(): Promise<Array<AdminEntity>> {
    return this.adminEntityRepository.find({
      where: {
        adminRoles: In( [AdminRole.ADMIN] ),
      },
      select: {
        id: true,
        displayName: true,
      },
    });
  }

but it throws Postgres error

SELECT "AdminEntity"."id" AS "AdminEntity_id", "AdminEntity"."display_name" AS "AdminEntity_display_name"
FROM "xxx"."admin" "AdminEntity"
WHERE ("AdminEntity"."admin_roles" IN ('ADMIN'))
[22P02] ERROR: malformed array literal: "ADMIN" Detail: Array value must start with "{" or dimension information. Position: 181

Have no idea how to fix this, please help

My Environment

Dependency Version
Operating System
Node.js version v16.7.0
Typescript version 4.6.2
TypeORM version 0.3.0
NestJS 8.4.1

Additional Context

Relevant Database Driver(s)

DB Type Reproducible
aurora-mysql no
aurora-postgres no
better-sqlite3 no
cockroachdb no
cordova no
expo no
mongodb no
mysql no
nativescript no
oracle no
postgres yes
react-native no
sap no
sqlite no
sqlite-abstract no
sqljs no
sqlserver no

Are you willing to resolve this issue by submitting a Pull Request?

  • ✖️ Yes, I have the time, and I know how to start.
  • ✖️ Yes, I have the time, but I don't know how to start. I would need guidance.
  • ✖️ No, I don’t have the time, but I can support (using donations) development.
  • ✖️ No, I don’t have the time and I’m okay to wait for the community / maintainers to resolve this issue.
@TrejGun
Copy link
Author

TrejGun commented Mar 19, 2022

Variations of OR syntax

      where: [{ adminRoles: AdminRole.ADMIN }],
      where: { adminRoles: `{${adminRoles.join(",")}}` },

does not work

@pleerock
Copy link
Member

what about

      where: {
        adminRoles: In(AdminRole.ADMIN),
      },

@TrejGun
Copy link
Author

TrejGun commented Mar 19, 2022

@pleerock

your option throws type error

TS2345: Argument of type 'AdminRole' is not assignable to parameter of type 'unknown[] | FindOperator '.

@TrejGun
Copy link
Author

TrejGun commented Mar 19, 2022

Query builder works but there are another 3 issues

  1. select does not apply naming strategy
  2. select does not accept an object
  3. select does not work at all, i.e. query returns empty array when it has select()
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    // queryBuilder.select(["id", "display_name"]); // <-- this line

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles });
      }
    }

    return queryBuilder.getMany();
  }

@pleerock
Copy link
Member

Can you also try:

      where: {
        adminRoles: Equal([AdminRole.ADMIN]),
      },

@TrejGun
Copy link
Author

TrejGun commented Mar 20, 2022

this also gives type error

TS2322: Type '{ adminRoles: EqualOperator<AdminRole[]>; }' is not assignable to type 'FindOptionsWhere<AdminEntity> | FindOptionsWhere<AdminEntity>[] | undefined'.   Types of property 'adminRoles' are incompatible.     Type 'EqualOperator<AdminRole[]>' is not assignable to type 'AdminRole | FindOperator<AdminRole.USER> | FindOperator<AdminRole.ADMIN> | FindOperator<AdminRole.MODERATOR> | undefined'.       Type 'EqualOperator<AdminRole[]>' is not assignable to type 'FindOperator<AdminRole.MODERATOR>'.         Types of property 'value' are incompatible.           Type 'AdminRole[]' is not assignable to type 'AdminRole.MODERATOR'.

@TrejGun
Copy link
Author

TrejGun commented Mar 20, 2022

@pleerock should I split this issue into two? enums and select?

@pleerock
Copy link
Member

Not really sure how it worked before, but the way you can use it now is to use new operators I added in #8766

@TrejGun
Copy link
Author

TrejGun commented Mar 21, 2022

will check and report back to you

@TrejGun
Copy link
Author

TrejGun commented Mar 21, 2022

@pleerock, new operators works, thanks

  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    return this.adminEntityRepository.find({
      where: {
        adminRoles: ArrayOverlap(adminRoles),
      },
      select: ["id", "displayName"],
    });
  }

however, there is still a broken SELECT function in the query builder

  1. select does not respect naming strategy
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    queryBuilder.select(["adminRoles"]);

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles });
      }
    }

    return queryBuilder.getMany();
  }

produces SQL

[Nest] 7733   - 2022-03-21 14:40:38 [TypeOrm] query failed: SELECT adminRoles FROM "XXX"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ [ 'ADMIN', 'MODERATOR' ] ]
[Nest] 7733   - 2022-03-21 14:40:38 [TypeOrm] error: error: column "adminroles" does not exist
[Nest] 7733   - 2022-03-21 14:40:38  column "adminroles" does not exist
  1. select without applying naming strategy with multiple elements returns no result
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    queryBuilder.select(["id", "admin_roles"]);
    // queryBuilder.select(["id"]);

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles });
      }
    }

    return queryBuilder.getMany();
  }

produces SQL

[Nest] 7744   - 2022-03-21 14:42:34 [TypeOrm] query : SELECT id, aadmin_roles FROM "XXX"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ [ 'ADMIN', 'MODERATOR' ] ]
  1. new function does not help
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    queryBuilder.select(["id", "admin_roles"]);

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles: ArrayOverlap(adminRoles) });
      }
    }

    return queryBuilder.getMany();
  }

throws error

[Nest] 7794   - 2022-03-21 14:47:23  malformed array literal: "{"_type":"arrayOverlap","_value":["ADMIN","MODERATOR"],"_useParameter":true,"_multipleParameters":false}"
  1. select without applying naming strategy with single elements returns no result
    same code as in 2 but another branch of code
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    queryBuilder.select(["id", "admin_roles"]);

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles });
      }
    }

    return queryBuilder.getMany();
  }

produces SQL

[Nest] 7836   - 2022-03-21 14:49:25 [TypeOrm] query : SELECT id, admin_roles FROM "XXX"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ 'ADMIN' ]
[Nest] 7836   - 2022-03-21 14:49:25 [TypeOrm] query failed: SELECT id, admin_roles FROM "XXX"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ 'ADMIN' ]
[Nest] 7836   - 2022-03-21 14:49:25 [TypeOrm] error: error: malformed array literal: "ADMIN"
[Nest] 7836   - 2022-03-21 14:49:25  malformed array literal: "ADMIN"
  1. select does not accept an object
  public async autocomplete(dto: IAdminAutocompleteDto): Promise<Array<AdminEntity>> {
    const { adminRoles = [] } = dto;
    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");

    queryBuilder.select({
      id: true
    });

    if (adminRoles) {
      if (adminRoles.length === 1) {
        queryBuilder.andWhere(":adminRoles = ANY(admin.adminRoles)", { adminRoles: adminRoles[0] });
      } else {
        queryBuilder.andWhere("admin.adminRoles && :adminRoles", { adminRoles });
      }
    }

    return queryBuilder.getMany();
  }

throws type error

src/admin/admin.service.ts(78,5): error TS2769: No overload matches this call.
  Overload 1 of 4, '(selection: (qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>, selectionAliasName?: string | undefined): SelectQueryBuilder<AdminEntity>', gave the following error.
    Argument of type '{ id: boolean; }' is not assignable to parameter of type '(qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>'.
      Object literal may only specify known properties, and 'id' does not exist in type '(qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>'.
  Overload 2 of 4, '(selection: string, selectionAliasName?: string | undefined): SelectQueryBuilder<AdminEntity>', gave the following error.
    Argument of type '{ id: boolean; }' is not assignable to parameter of type 'string'.
  Overload 3 of 4, '(selection: string[]): SelectQueryBuilder<AdminEntity>', gave the following error.
    Argument of type '{ id: boolean; }' is not assignable to parameter of type 'string[]'.
      Object literal may only specify known properties, and 'id' does not exist in type 'string[]'.

@pleerock
Copy link
Member

you don't use select correctly. You must use aliases in your selects, e.g.:

    const queryBuilder = this.adminEntityRepository.createQueryBuilder("admin");
    queryBuilder.select(["admin.id", "admin.admin_roles"]);

@TrejGun
Copy link
Author

TrejGun commented Mar 22, 2022

@pleerock

ok, this approach works

queryBuilder.select(["admin.adminRoles"]);
query : SELECT "admin"."admin_roles" AS "admin_admin_roles", "admin"."id" AS "admin_id" FROM "areti"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ [ 'ADMIN', 'MODERATOR' ] ]

and this does not

queryBuilder.select(["admin.admin_roles"]);
[Nest] 8538   - 2022-03-22 00:03:12 [TypeOrm] query : SELECT "admin"."admin_roles" FROM "areti"."admin" "admin" WHERE "admin"."admin_roles" && $1  -- PARAMETERS: [ [ 'ADMIN', 'MODERATOR' ] ]

I mean query is correct but it seems the result is not mapped to entity because of the difference in fields name admin_admin_roles vs admin_roles

is this correct behavior?

PS
the last one about malformed array literal was related to swagger
OAI/OpenAPI-Specification#1706

@pleerock
Copy link
Member

I mean query is correct but it seems the result is not mapped to entity because of the difference in fields name admin_admin_roles vs admin_roles

are you talking about alias name? alias name can be any.

you don't have entity mapped because you did not select primary columns, they are required for selection if you want entities to be properly mapped and returned

@TrejGun
Copy link
Author

TrejGun commented Mar 22, 2022

thanks. this works

@TrejGun TrejGun closed this as completed Mar 22, 2022
@msanandrea
Copy link

I'm having a similar issue described here => https://stackoverflow.com/questions/78511788/typeorm-findoptionswhere-throws-incompatible-error-on-enums-array

I'm using version 0.3.2 and the problem persists

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants