ESLint
Static Better Drizzle guardrails for IDE and CI feedback.
better-drizzle/eslint is the static surface for Better Drizzle guardrails. It mirrors the directly-checkable subset of better-drizzle/rules and is meant for editor feedback and CI, not runtime enforcement.
Install
npm install -D eslint @typescript-eslint/parser better-drizzlepnpm add -D eslint @typescript-eslint/parser better-drizzleyarn add -D eslint @typescript-eslint/parser better-drizzlebun add -d eslint @typescript-eslint/parser better-drizzleFlat Config
import parser from '@typescript-eslint/parser';
import betterDrizzle from 'better-drizzle/eslint';
export default [
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parser,
sourceType: 'module',
},
plugins: {
'better-drizzle': betterDrizzle,
},
rules: {
...betterDrizzle.configs.recommended[0].rules,
},
},
];The package also exports safe, recommended, and strict flat-config arrays.
What It Checks
The ESLint plugin only analyzes direct and obvious Better Drizzle call sites such as:
client.users.findMany(...)client.repository('users').updateMany(...)client.$rawUnsafe(...)client.users.$withoutPlugins().findMany(...)
It does not try to infer transaction state, runtime meta, dialect-specific behavior, or complex wrapper functions.
Rules
Rule IDs use the better-drizzle/ prefix. The preset columns show the level each preset sets, with its options where they matter.
Destructive writes
| Rule | Reports | safe | recommended | strict |
|---|---|---|---|---|
better-drizzle/no-delete-many-without-where | deleteMany without where | error | error | error |
better-drizzle/no-update-many-without-where | updateMany without where | error | error | error |
better-drizzle/no-delete-without-where | delete without where | error | error | error |
better-drizzle/no-update-without-where | update without where | error | error | error |
better-drizzle/no-empty-where | an empty where on the listed operations | error | error | error |
Reads and pagination
| Rule | Reports | safe | recommended | strict |
|---|---|---|---|---|
better-drizzle/no-unbounded-find-many | findMany with no limit or take (safe and recommended also accept a where) | warn | warn | error |
better-drizzle/require-explicit-limit | listed operations with no limit, take, perPage, first, or last | off | off | warn |
better-drizzle/max-limit | a literal limit above value | warn (1000) | warn (1000) | error (500) |
better-drizzle/require-order-by-for-limit | a limit without orderBy | off | off | warn |
better-drizzle/require-order-by-for-pagination | paginate without orderBy | off | off | warn |
better-drizzle/require-order-by-for-cursor | cursor without orderBy | error | error | error |
better-drizzle/require-stable-order-by-for-cursor | cursor whose orderBy lacks id, only when requirePrimaryKeyInOrderBy: true | off | warn | error |
Includes and locks
| Rule | Reports | safe | recommended | strict |
|---|---|---|---|---|
better-drizzle/max-include-depth | include nested deeper than value | off | warn (3) | error (2) |
better-drizzle/max-include-relations | more include relations than value | off | warn (5) | error (3) |
better-drizzle/no-lock-with-include | lock together with include | error | error | error |
better-drizzle/no-invalid-lock-combination | skipLocked together with noWait | error | error | error |
better-drizzle/require-order-by-for-skip-locked | skipLocked without orderBy | off | warn | error |
better-drizzle/require-limit-for-skip-locked | skipLocked without a limit | error | error | error |
Raw SQL
| Rule | Reports | safe | recommended | strict |
|---|---|---|---|---|
better-drizzle/no-raw-unsafe | any $rawUnsafe(...) call | error | error | error |
better-drizzle/require-raw-comment | a raw call without a comment of at least minLength characters | off | off | warn (8) |
better-drizzle/require-raw-timeout | a raw call without timeoutMs, or above defaultTimeoutMs / maxTimeoutMs | off | warn | error |
better-drizzle/no-raw-mutation | raw SQL starting with a write verb, unless it starts with an allow prefix | off | warn | error |
Sensitive fields and plugin bypass
| Rule | Reports | safe | recommended | strict |
|---|---|---|---|---|
better-drizzle/no-sensitive-select | select with a sensitive field set to true (the presets allow it when the call passes withSensitive: true) | off | error | error |
better-drizzle/require-explicit-sensitive-access-reason | a sensitive select with withSensitive: true but no reason string | off | off | error |
better-drizzle/no-without-plugins | any $withoutPlugins() call | off | warn | warn |
better-drizzle/no-plugin-bypass-without-reason | a $withoutPlugins() call whose args have no reason string | off | warn | error |
The sensitive field list is fixed: apiKey, apiKeys, password, passwordHash, secret, secrets, ssn, token, tokens. Only literal true values in an inline select object are matched.
Configuring rules
In an ESLint rules block, use standard ESLint severities: 'off', 'warn', 'error' (or 0, 1, 2), or [severity, options]. The true / false and { level } shorthands belong to the runtime better-drizzle/rules plugin and do not work here.
import betterDrizzle from 'better-drizzle/eslint';
export default [
...betterDrizzle.configs.recommended,
{
rules: {
'better-drizzle/no-without-plugins': 'off',
'better-drizzle/max-limit': ['error', { value: 200 }],
'better-drizzle/no-raw-mutation': ['warn', { allow: ['refresh materialized view'] }],
},
},
];Rule options:
| Rule | Options |
|---|---|
no-empty-where | operations, treatUndefinedAsEmpty, treatEmptyAndOrAsEmpty |
no-unbounded-find-many | allowWithWhere, allowWithLimit, allowWithTake, allowSmallStaticModels |
require-explicit-limit | operations |
max-limit | value, applyTo |
require-stable-order-by-for-cursor | requirePrimaryKeyInOrderBy |
max-include-depth / max-include-relations | value |
require-raw-comment | minLength |
require-raw-timeout | defaultTimeoutMs, maxTimeoutMs |
no-raw-mutation | allow |
no-sensitive-select | allowWithSensitive, withSensitiveArg |
require-explicit-sensitive-access-reason | withSensitiveArg, reasonArg |
no-plugin-bypass-without-reason | reasonArg |
The other rules take no options.
Presets
safe, recommended, and strict are flat-config arrays, also available as configs.safe, configs.recommended, and configs.strict. Each one sets the TypeScript parser for **/*.{ts,tsx,mts,cts}, registers the plugin, and enables the levels above.
They are generated from the runtime presets of better-drizzle/rules, limited to the statically checkable rules. Runtime-only rules such as tenant context, transaction state, and dialect handling stay in better-drizzle/rules.
Example
import { recommended } from 'better-drizzle/eslint';
export default [
...recommended,
];Boundary
Use better-drizzle/eslint when you want immediate feedback in Zed, VS Code, or CI. Use better-drizzle/rules when you need runtime enforcement inside the actual Better Drizzle client.