# Lint rules

> Catch unbounded reads and unscoped deletes in the editor, with ESLint or oxlint.

Source: https://bettersupabase.com/docs/repository/lint

`better-supabase/lint` is a lint plugin written against the ESLint rule API.
It has no dependencies and loads in ESLint's flat config and in oxlint's JS
plugins.

## ESLint [#eslint]

```js title="eslint.config.js"
import betterSupabase from "better-supabase/lint";

export default [betterSupabase.configs.recommended];
```

## oxlint [#oxlint]

```json title=".oxlintrc.json"
{
  "jsPlugins": ["better-supabase/lint"],
  "rules": {
    "better-supabase/no-empty-where": "error",
    "better-supabase/no-unbounded-list": "warn",
    "better-supabase/max-limit": ["warn", { "max": 500 }],
    "better-supabase/require-order-by": "warn",
    "better-supabase/unbounded-read": ["warn", { "tables": ["public.events"] }]
  }
}
```

## Rules [#rules]

### no-unbounded-list [#no-unbounded-list]

`list()` or `list({ ... })` without `limit`. Recommended: `warn`.

### no-empty-where [#no-empty-where]

`update({ where: {} }, patch)` or `delete({ where: {} })`, a filter that
matches every row. The repository refuses this at runtime too, unless the call
passes `allowAll: true`; the rule catches it before it ships. Recommended:
`error`.

### max-limit [#max-limit]

A literal `limit` above `max` (default 1000) in `list` or `page`.
Recommended: `warn`.

### require-order-by [#require-order-by]

`list` with `offset` but no `orderBy`, which returns pages in no stable
order. Recommended: `warn`.

### require-max-affected [#require-max-affected]

`update({ where }, patch)` or `delete({ where })` without
[`maxAffected`](/docs/repository/writing#limiting-bulk-writes), or with a
literal `maxAffected` above `max` (default 1000). Not in `recommended`; turn
it on where a bulk write that matches too many rows would hurt:

```js title="eslint.config.js"
export default [
  betterSupabase.configs.recommended,
  {
    rules: { "better-supabase/require-max-affected": ["error", { max: 500 }] },
  },
];
```

### unbounded-read [#unbounded-read]

`db.<table>.list` without `limit` on a large table. PostgREST cuts such a
read off at `db-max-rows` without an error (see
[row caps](/docs/repository/pagination#row-caps)). Recommended: `warn`, but
it only reports once it knows which tables are large:

- `tables`: the large tables. `largeTables(snapshot)` reads them from
  `supabase/snapshot.json`, where `gen` marks every table Postgres estimates
  at 10,000 rows or more. Re-run `gen` against a database with realistic
  data (or production) to update the marks.
- `strict: true`: report on every table, like `no-unbounded-list`.

```js title="eslint.config.js"
import betterSupabase, { largeTables } from "better-supabase/lint";
import snapshot from "./supabase/snapshot.json" with { type: "json" };

export default [
  betterSupabase.configs.recommended,
  {
    rules: {
      "better-supabase/unbounded-read": [
        "warn",
        { tables: largeTables(snapshot) },
      ],
    },
  },
];
```

Table names match regardless of casing: `order_items`, `orderItems` and
`public.order_items` are the same table.

### require-escape-like [#require-escape-like]

A `where` value for `like` or `ilike` that comes from a variable, a property,
or a template or `+` concatenation, without `escapeLike(...)` around it. A
`%` or `_` in user input then acts as a wildcard, so `ilike: email` matches
other users' addresses. Use `ieq` for case-insensitive equality, `contains`
or `startsWith` for a substring or prefix, or wrap the value in
`escapeLike()` when you build the pattern yourself. String literals pass.
Recommended: `warn`. From 0.11.

```ts
where: {
  email: {
    ieq: email;
  }
}
where: {
  name: {
    ilike: `${escapeLike(prefix)}%`;
  }
}
```

## How calls are matched [#how-calls-are-matched]

Rules match calls by method name with an inline object literal. When the
argument is a variable or contains a spread, the rule skips it instead of
guessing, so there are no false positives from code it can't see. The
runtime [`guards()`](/docs/repository/guards) check what the linter can't.