# SQL 拦截器（实体过滤器）

有些条件应该**对某实体的所有查询生效**——软删除（排除已删行）、多租户、行级权限。手写进每个查询容易漏；`FilterManager` 把它做成机制：注册一次，自动注入所有查询。

## 沙盒真实演示：软删除 task

示例数据中有 `task` 表（id/title/deleted_at，含一行已软删除），下面的完整示例可直接运行：

```ts workspace
// ① 导入：model/prop/dto 建模型；FilterManager 从 @ts-grm/sql 导入；
//    createFilteredClient 是示例提供的 client 工厂（仅可带 filterManager）
import { model, prop, dto } from '@ts-grm/core'
import { FilterManager } from '@ts-grm/sql'
import { createFilteredClient, sqlClient } from './infra/sql-client'

// ② 模型：task 含 deletedAt（时间戳；非空 = 已软删除）
const Task = model("Task", "id", class {
    id = prop.i64()
    title = prop.str(50)
    deletedAt = prop.dt().nullable()
})

// ③ 注册软删除过滤器：谓词 = deleted_at is null
const filterManager = new FilterManager()
filterManager.add(Task, t => t.deletedAt.isNull())

// ④ 用带过滤器的 client 查询：所有 task 查询自动 AND deleted = false
const client = createFilteredClient({ filterManager })
const tasks = await client.createQuery(Task, (q, t) => {
    return q.select(t.fetch(dto.view(Task, c => [c.id, c.title])))
}).fetchList()

console.log('过滤器 client（自动排除已删）:')
console.log(tasks)

// ⑤ 对照：默认 client 无过滤器，能看到全部（含已删行）
const all = await sqlClient.createQuery(Task, (q, t) => {
    return q.select(t.fetch(dto.view(Task, c => [c.id, c.title])))
}).fetchList()

console.log('默认 client（含已删行）:', all)
```

运行结果：过滤器 client 只返回未删除的 2 行（“旧需求”被自动排除），默认 client 返回 3 行含已删——**同一个模型，有无拦截器带来不同查询语义**。

## 机制

```ts
const filterManager = new FilterManager()
filterManager.add(MODEL, table => 谓词)     // Filter = (table) => Predicate | undefined
const client = newSqlClient(driver, { filterManager })   // 挂到 client 即全量生效
```

- 谓词与查询内 `q.where(...)` **AND 叠加**，对 findMany/createQuery/子查询/计数全部生效；
- 返回 `undefined` 表示该查询不附加约束（可选过滤）；
- 示例工作区提供了 `createFilteredClient`（仅透传 `filterManager`）；真实项目直接用 `newSqlClient` 的 `filterManager` 选项。

## 典型场景

| 场景 | 过滤器谓词 |
|---|---|
| 软删除 | `row => row.deletedAt.isNull()` |
| 多租户行隔离 | `row => row.tenantId.eq(currentTenant)` |
| 行级权限 | `row => row.ownerId.in(currentUserVisibleIds)` |
| 默认数据范围 | `row => row.status.ne("ARCHIVED")` |

把"惯例"沉淀成拦截器后，新写查询天然合规——少一个忘记过滤的坑。

## 临时放行

要看到被过滤掉的数据（管理后台查已删行），按放行范围选两条路：

- **单次关联访问放行**：关联属性调用时传 `{ ignoreTargetFilters: true }`，该次访问不再给**目标模型**注入过滤器（源模型自身的过滤器不受影响）：

  ```ts
  // Author 上注册了过滤器；这次按 id 找作者不套用 Author 的过滤器
  q.where(book.author({ ignoreTargetFilters: true }).id.eq(2))
  ```

- **按 client 分权**：给"默认 client"与"管理 client"各配一个 `filterManager`（`newSqlClient` 选项不同），调用点显式选择——边界落在"哪个 client"上，比散装绕过着更清楚。

两者都是**显式**放行：范围一眼可读（一次关联访问 / 一个 client）。FilterManager 本身不提供"临时禁用某个过滤器"的全局开关。

## 下一步

SQL 后端七页收束：驱动 → 命名 → 事务 → 方言 → 日志 → 模式创建 → 拦截器。[「API 参考」](../reference/core)提供速查。
