Every TypeORM project I've worked on grows the same few dangerous lines. I got tired of catching them by hand, so I wrote a linter that does it for me.
I was reviewing a pull request a while back and nearly scrolled past this line:
await manager.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
Looks fine at a glance. It's a SQL injection hole. The id comes off the request and lands directly in the query string.
The thing that bugged me is that I only caught it because I happened to be reading carefully on that line, that day. Review catches this sort of thing a lot of the time. "A lot of the time" is how these end up in production.
It's usually not only injection either. The same projects tend to collect a few other habits:
-
synchronize: truein a data source config. It rewrites your schema on startup and can drop a column on the next deploy. - A QueryBuilder
delete()orupdate()that hits.execute()with no.where(). Forget that one line and you've changed every row in the table. - Three or four writes in one function, none in a transaction, so if the second throws you're left with half-written data.
- On multi-tenant apps, a query that forgets the tenant filter. That's how one customer sees another customer's data.
None of these really need a person to catch them. They're mechanical. A linter can do it on every commit.
So I built one.
eslint-plugin-typeorm-enterprise
npm install --save-dev eslint eslint-plugin-typeorm-enterprise
// eslint.config.js
const typeormEnterprise = require('eslint-plugin-typeorm-enterprise');
module.exports = [typeormEnterprise.configs.recommended];
Now those patterns are lint errors. Ten rules today, split into configs (recommended, strict, performance, multiTenant): raw SQL, interpolated and concatenated SQL, unsafe QueryBuilder deletes, EntityManager raw queries, writes outside a transaction, tenant scoping, and a nudge to stop counting rows when you only need to know one exists.
Two things I was picky about
It doesn't need any type setup. The rules read the syntax tree, so they work in a plain ESLint 9 config with nothing else to wire up, and they run under oxlint too. If you already lint with type information, there's an optional mode that checks the receiver is actually a TypeORM Repository or EntityManager instead of matching on a name.
And it tries hard not to be noisy. The fastest way to get a linter switched off is a wall of false positives on the first run. This one leaves req.query.id and other non-SQL calls alone.
Try it
- npm: https://www.npmjs.com/package/eslint-plugin-typeorm-enterprise
- GitHub: https://github.com/alokraj68/eslint-plugin-typeorm-enterprise
Rule ideas and false-positive reports welcome, especially real TypeORM footguns you've hit.
Top comments (2)
Spot on. Those recurring patterns bite every project. Add a quick checklist: parameterized queries, never build SQL strings, avoid JSON.parse on localStorage, and use tokens for auth instead of local state. Small reviews, big safety. π
This is the right instinct. If a class of bug survives because someone happened to be tired, fast-scrolling, or reviewing a large diff, then it does not belong in the "humans will catch it" bucket. The line you called out is exactly the kind of thing that looks harmless in context and then becomes obvious only after the incident.
I also like that you widened the lens beyond injection to the surrounding TypeORM habits that quietly accumulate in real codebases. That is usually the more useful move: encode the insecure pattern family once, instead of hoping reviewers remember every variant. Curious whether your linter is also catching raw query fragments built indirectly through helper functions, since that is where a lot of teams lose the simple string-interpolation signal.