Files
ent/doc/md/interceptors.mdx
2023-01-14 22:47:54 +02:00

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>