findBy with a primitive under a relation key silently returns all rows (no WHERE clause)
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 clausefindOneBy(Post, { author: 1 })→ returns an arbitrary rowcountBy(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
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.
Source: typeorm/typeorm