mirror of
https://github.com/ent/ent.git
synced 2026-03-05 19:35:23 +03:00
416 lines
12 KiB
Plaintext
416 lines
12 KiB
Plaintext
---
|
|
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> |