snapshot of https://github.com/ent/ent.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
416
doc/md/interceptors.mdx
Normal file
416
doc/md/interceptors.mdx
Normal file
@@ -0,0 +1,416 @@
|
||||
---
|
||||
id: interceptors
|
||||
title: Interceptors
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
Interceptors are execution middleware for various types of Ent queries. Contrary to hooks, interceptors are applied on
|
||||
the read-path and implemented as interfaces, allows them to intercept and modify the query at different stages, providing
|
||||
more fine-grained control over queries' behavior. For example, see the [Traverser interface](#defining-a-traverser) below.
|
||||
|
||||
## Defining an Interceptor
|
||||
|
||||
To define an `Interceptor`, users can declare a struct that implements the `Intercept` method or use the predefined
|
||||
`ent.InterceptFunc` adapter.
|
||||
|
||||
```go
|
||||
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
|
||||
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
|
||||
// Do something before the query execution.
|
||||
value, err := next.Query(ctx, query)
|
||||
// Do something after the query execution.
|
||||
return value, err
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, the `ent.Query` represents a generated query builder (e.g., `ent.<T>Query`) and accessing its
|
||||
methods requires type assertion. For example:
|
||||
|
||||
```go
|
||||
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
|
||||
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
|
||||
if q, ok := query.(*ent.UserQuery); ok {
|
||||
q.Where(user.Name("a8m"))
|
||||
}
|
||||
return next.Query(ctx, query)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
However, the utilities generated by the `intercept` feature flag enable the creation of generic interceptors that can
|
||||
be applied to any query type. The `intercept` feature flag can be added to a project in one of two ways:
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="cli" label="CLI" default>
|
||||
|
||||
If you are using the default go generate config, add `--feature intercept` option to the `ent/generate.go` file as follows:
|
||||
|
||||
```go title="ent/generate.go"
|
||||
package ent
|
||||
|
||||
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept ./schema
|
||||
```
|
||||
|
||||
It is recommended to add the [`schema/snapshot`](features.md#auto-solve-merge-conflicts) feature-flag along with the
|
||||
`intercept` flag to enhance the development experience, for example:
|
||||
|
||||
```go
|
||||
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature intercept,schema/snapshot ./schema
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="entc" label="Go">
|
||||
|
||||
If you are using the configuration from the GraphQL documentation, add the feature flag as follows:
|
||||
|
||||
```go
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"entgo.io/ent/entc"
|
||||
"entgo.io/ent/entc/gen"
|
||||
)
|
||||
|
||||
func main() {
|
||||
opts := []entc.Option{
|
||||
entc.FeatureNames("intercept"),
|
||||
}
|
||||
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
|
||||
log.Fatalf("running ent codegen: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
It is recommended to add the [`schema/snapshot`](features.md#auto-solve-merge-conflicts) feature-flag along with the
|
||||
`intercept` flag to enhance the development experience, for example:
|
||||
|
||||
```diff
|
||||
opts := []entc.Option{
|
||||
- entc.FeatureNames("intercept"),
|
||||
+ entc.FeatureNames("intercept", "schema/snapshot"),
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Interceptors Registration
|
||||
|
||||
:::important
|
||||
You should notice that similar to [schema hooks](hooks.md#hooks-registration), if you use the **`Interceptors`** option
|
||||
in your schema, you **MUST** add the following import in the main package, because a circular import is possible between
|
||||
the schema package and the generated ent package:
|
||||
```go
|
||||
import _ "<project>/ent/runtime"
|
||||
```
|
||||
:::
|
||||
|
||||
#### Using the generated `intercept` package
|
||||
|
||||
Once the feature flag was added to your project, the creation of interceptors is possible using the `intercept` package:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="func" label="intercept.Func" default>
|
||||
|
||||
```go
|
||||
client.Intercept(
|
||||
intercept.Func(func(ctx context.Context, q intercept.Query) error {
|
||||
// Limit all queries to 1000 records.
|
||||
q.Limit(1000)
|
||||
return nil
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="traverse-func" label="intercept.TraverseFunc">
|
||||
|
||||
```go
|
||||
client.Intercept(
|
||||
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
|
||||
// Apply a predicate/filter to all queries.
|
||||
q.WhereP(predicate)
|
||||
return nil
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="new-query" label="intercept.NewQuery">
|
||||
|
||||
```go
|
||||
ent.InterceptFunc(func(next ent.Querier) ent.Querier {
|
||||
return ent.QuerierFunc(func(ctx context.Context, query ent.Query) (ent.Value, error) {
|
||||
// Get a generic query from a typed-query.
|
||||
q, err := intercept.NewQuery(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.Limit(1000)
|
||||
return next.Intercept(ctx, query)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Defining a Traverser
|
||||
|
||||
In some cases, there is a need to intercept [graph traversals](traversals.md) and modify their builders before
|
||||
continuing to the nodes returned by the query. For example, in the query below, we want to ensure that only `active`
|
||||
users are traversed in **any** graph traversals in the system:
|
||||
|
||||
```go
|
||||
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
|
||||
q.Where(user.Active(true))
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
After defining and registering such Traverser, it will take effect on all graph traversals in the system. For example:
|
||||
|
||||
```go
|
||||
func TestTypedTraverser(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&_fk=1")
|
||||
defer client.Close()
|
||||
a8m, nat := client.User.Create().SetName("a8m").SaveX(ctx), client.User.Create().SetName("nati").SetActive(false).SaveX(ctx)
|
||||
client.Pet.CreateBulk(
|
||||
client.Pet.Create().SetName("a").SetOwner(a8m),
|
||||
client.Pet.Create().SetName("b").SetOwner(a8m),
|
||||
client.Pet.Create().SetName("c").SetOwner(nat),
|
||||
).ExecX(ctx)
|
||||
|
||||
// highlight-start
|
||||
// Get pets of all users.
|
||||
if n := client.User.Query().QueryPets().CountX(ctx); n != 3 {
|
||||
t.Errorf("got %d pets, want 3", n)
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
// Add an interceptor that filters out inactive users.
|
||||
client.User.Intercept(
|
||||
intercept.TraverseUser(func(ctx context.Context, q *ent.UserQuery) error {
|
||||
q.Where(user.Active(true))
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
// highlight-start
|
||||
// Only pets of active users are returned.
|
||||
if n := client.User.Query().QueryPets().CountX(ctx); n != 2 {
|
||||
t.Errorf("got %d pets, want 2", n)
|
||||
}
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
## Interceptors vs. Traversers
|
||||
|
||||
Both `Interceptors` and `Traversers` can be used to modify the behavior of queries, but they do so at different stages
|
||||
the execution. Interceptors function as middleware and allow modifying the query before it is executed and modifying
|
||||
the records after they are returned from the database. For this reason, they are applied only in the final stage of the
|
||||
query - during the actual execution of the statement on the database. On the other hand, Traversers are called one stage
|
||||
earlier, at each step of a graph traversal allowing them to modify both intermediate and final queries before they
|
||||
are joined together.
|
||||
|
||||
In summary, a Traverse function is a better fit for adding default filters to graph traversals while using an Intercept
|
||||
function is better for implementing logging or caching capabilities to the application.
|
||||
|
||||
```go
|
||||
client.User.Query().
|
||||
QueryGroups(). // User traverse functions applied.
|
||||
QueryPosts(). // Group traverse functions applied.
|
||||
All(ctx) // Post traverse and intercept functions applied.
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Soft Delete
|
||||
|
||||
The soft delete pattern is a common use-case for interceptors and hooks. The example below demonstrates how to add such
|
||||
functionality to all schemas in the project using [`ent.Mixin`](schema-mixin.md):
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="mixin" label="Mixin" default>
|
||||
|
||||
```go
|
||||
// SoftDeleteMixin implements the soft delete pattern for schemas.
|
||||
type SoftDeleteMixin struct {
|
||||
mixin.Schema
|
||||
}
|
||||
|
||||
// Fields of the SoftDeleteMixin.
|
||||
func (SoftDeleteMixin) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
field.Time("delete_time").
|
||||
Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
type softDeleteKey struct{}
|
||||
|
||||
// SkipSoftDelete returns a new context that skips the soft-delete interceptor/mutators.
|
||||
func SkipSoftDelete(parent context.Context) context.Context {
|
||||
return context.WithValue(parent, softDeleteKey{}, true)
|
||||
}
|
||||
|
||||
// Interceptors of the SoftDeleteMixin.
|
||||
func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
|
||||
return []ent.Interceptor{
|
||||
intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
|
||||
// Skip soft-delete, means include soft-deleted entities.
|
||||
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
|
||||
return nil
|
||||
}
|
||||
d.P(q)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks of the SoftDeleteMixin.
|
||||
func (d SoftDeleteMixin) Hooks() []ent.Hook {
|
||||
return []ent.Hook{
|
||||
hook.On(
|
||||
func(next ent.Mutator) ent.Mutator {
|
||||
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
|
||||
// Skip soft-delete, means delete the entity permanently.
|
||||
if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
|
||||
return next.Mutate(ctx, m)
|
||||
}
|
||||
mx, ok := m.(interface {
|
||||
SetOp(ent.Op)
|
||||
Client() *gen.Client
|
||||
SetDeleteTime(time.Time)
|
||||
WhereP(...func(*sql.Selector))
|
||||
})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected mutation type %T", m)
|
||||
}
|
||||
d.P(mx)
|
||||
mx.SetOp(ent.OpUpdate)
|
||||
mx.SetDeleteTime(time.Now())
|
||||
return mx.Client().Mutate(ctx, m)
|
||||
})
|
||||
},
|
||||
ent.OpDeleteOne|ent.OpDelete,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// P adds a storage-level predicate to the queries and mutations.
|
||||
func (d SoftDeleteMixin) P(w interface{ WhereP(...func(*sql.Selector)) }) {
|
||||
w.WhereP(
|
||||
sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="schema" label="Mixin usage">
|
||||
|
||||
```go
|
||||
// Pet holds the schema definition for the Pet entity.
|
||||
type Pet struct {
|
||||
ent.Schema
|
||||
}
|
||||
|
||||
// Mixin of the Pet.
|
||||
func (Pet) Mixin() []ent.Mixin {
|
||||
return []ent.Mixin{
|
||||
//highlight-next-line
|
||||
SoftDeleteMixin{},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="runtime" label="Runtime usage">
|
||||
|
||||
```go
|
||||
// Filter out soft-deleted entities.
|
||||
pets, err := client.Pet.Query().All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Include soft-deleted entities.
|
||||
pets, err := client.Pet.Query().All(schema.SkipSoftDelete(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Limit number of records
|
||||
|
||||
The following example demonstrates how to limit the number of records returned from the database using an interceptor
|
||||
function:
|
||||
|
||||
```go
|
||||
client.Intercept(
|
||||
intercept.Func(func(ctx context.Context, q intercept.Query) error {
|
||||
// LimitInterceptor limits the number of records returned from
|
||||
// the database to 1000, in case Limit was not explicitly set.
|
||||
if ent.QueryFromContext(ctx).Limit == nil {
|
||||
q.Limit(1000)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
### Multi-project support
|
||||
|
||||
The example below demonstrates how to write a generic interceptor that can be used in multiple projects:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="definition" label="Definition">
|
||||
|
||||
```go
|
||||
// Project-level example. The usage of "entgo" package emphasizes that this interceptor does not rely on any generated code.
|
||||
func SharedLimiter[Q interface{ Limit(int) }](f func(entgo.Query) (Q, error), limit int) entgo.Interceptor {
|
||||
return entgo.InterceptFunc(func(next entgo.Querier) entgo.Querier {
|
||||
return entgo.QuerierFunc(func(ctx context.Context, query entgo.Query) (entgo.Value, error) {
|
||||
l, err := f(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Limit(limit)
|
||||
// LimitInterceptor limits the number of records returned from the
|
||||
// database to the configured one, in case Limit was not explicitly set.
|
||||
if entgo.QueryFromContext(ctx).Limit == nil {
|
||||
l.Limit(limit)
|
||||
}
|
||||
return next.Query(ctx, query)
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="usage" label="Usage">
|
||||
|
||||
```go
|
||||
client1.Intercept(SharedLimiter(intercept1.NewQuery, limit))
|
||||
|
||||
client2.Intercept(SharedLimiter(intercept2.NewQuery, limit))
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
Reference in New Issue
Block a user