#12712·typeorm

findBy with a primitive under a relation key silently returns all rows (no WHERE clause)

Author: IbrahimHafez1Created Jul 18, 2026Updated Sep 10, 2026

Issue description

A primitive value under a relation key in a find where (e.g. findBy(Post, { author: 1 })) is silently dropped: the generated query has no WHERE clause and returns every row in the table.

Expected Behavior

findBy(Post, { author: 1 }) should either filter by the relation id (equivalent to { author: { id: 1 } }) or raise a clear error — it should not silently return the whole table. (Which of the two is the intended behavior is an open question for the maintainers — see "Suggested fix" below.)

Actual Behavior

The criterion vanishes and the query returns all rows:

  • findBy(Post, { author: 1 }) → returns every post, SQL: SELECT ... FROM "post" "Post" INNER JOIN "author" ... with no WHERE clause
  • findOneBy(Post, { author: 1 }) → returns an arbitrary row
  • countBy(Post, { author: 1 }) / existsBy(...) → operate on the whole table

This is a silent-wrong-data bug: in a multi-tenant/per-user query like findOneBy({ user: userId }), it returns some other user's row instead of the intended one.

Note: this shorthand is not part of the documented/typed API — the docs only show the nested form (author: { id: 1 }), and FindOptionsWhere does not permit a bare primitive under a relation key. So this is reached from plain-JS callers, as any, or code that assumed the shorthand works because it happens to be accepted on the delete/update criteria path. It is reported as a bug because the failure mode is "silently return everything" rather than a clear rejection.

Related but distinct: #11873 (findOneBy({ id: undefined }) returns the first record) — same "silent return-all" family, different trigger.

Steps to reproduce

javascript
require("reflect-metadata")
const { DataSource, EntitySchema } = require("typeorm")

const Author = new EntitySchema({
    name: "Author",
    columns: {
        id: { type: Number, primary: true, generated: true },
        name: { type: String },
    },
})
const Post = new EntitySchema({
    name: "Post",
    columns: {
        id: { type: Number, primary: true, generated: true },
        title: { type: String },
    },
    relations: { author: { type: "many-to-one", target: "Author" } },
})

async function main() {
    const ds = new DataSource({
        type: "better-sqlite3",
        database: ":memory:",
        synchronize: true,
        entities: [Post, Author],
    })
    await ds.initialize()
    const posts = ds.getRepository("Post")
    const authors = ds.getRepository(Author)

    const a1 = await authors.save({ name: "a1" })
    const a2 = await authors.save({ name: "a2" })
    await posts.save([
        { title: "a1-post-1", author: a1 },
        { title: "a1-post-2", author: a1 },
        { title: "a2-post-1", author: a2 },
    ])

    // expect posts by author 1 only; actually returns all 3
    console.log((await posts.findBy({ author: 1 })).map((p) => p.title))
    // -> [ 'a1-post-1', 'a1-post-2', 'a2-post-1' ]

    await ds.destroy()
}
main()

Root cause

In SelectQueryBuilder.buildWhere (src/query-builder/SelectQueryBuilder.ts), the relation branch handles null, all-undefined nested objects, and FindOperators explicitly, then falls through to a branch that assumes the value is a nested criteria object: it pushes a join and recurses buildWhere(value, relationMetadata, joinAlias). When value is a primitive, the recursion's for (const key in value) iterates nothing, returns "", and the empty condition is discarded — so the join is added but no predicate, yielding an unfiltered query. (A string value instead throws a misleading EntityPropertyNotFoundError("0") because for..in over a string iterates its indices.)

Suggested fix (two options — maintainers' call)

Both options keep the existing author: true behavior (inner-join to the relation) untouched.

Option A — reject the invalid input (throw): when a primitive is passed under a relation key, throw a TypeORMError pointing to the { id: value } form. Consistent with the documented API and the invalidWhereValuesBehavior direction of turning silently-dropped criteria into loud errors.

Option B — support the shorthand: treat { author: 1 } as { author: { id: 1 } } by building the equality on the relation's join column for the owning side (many-to-one / owning one-to-one), and throw for relations that don't hold the join column (one-to-many, many-to-many). Matches how the same criterion resolves on the delete/update path.

Happy to open a PR for whichever direction you prefer (a working branch with tests exists for Option B; Option A is a small change from it).

My Environment

Dependency Version
Operating System Windows 11
Node.js version 22.17.1
Typescript version 5.9.3
TypeORM version 1.1.0 (reproduced on current master)

Relevant Database Driver(s)

All drivers — driver-agnostic query-builder code (reproduced with better-sqlite3).

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

Yes, I have the time, and I know how to start — but I'd like the maintainers to choose between Option A and Option B first, since it's an API-behavior decision.