ent: export query interceptors (#3157)

This commit is contained in:
Ariel Mashraki
2022-12-19 10:17:10 +02:00
committed by GitHub
parent 3328201ba8
commit f226627d67
493 changed files with 22829 additions and 10766 deletions

View File

@@ -31,7 +31,7 @@ type Client struct {
// NewClient creates a new client configured with the given options.
func NewClient(opts ...Option) *Client {
cfg := config{log: log.Println, hooks: &hooks{}}
cfg := config{log: log.Println, hooks: &hooks{}, inters: &inters{}}
cfg.options(opts...)
client := &Client{config: cfg}
client.init()
@@ -126,6 +126,12 @@ func (c *Client) Use(hooks ...Hook) {
c.User.Use(hooks...)
}
// Intercept adds the query interceptors to all the entity clients.
// In order to add interceptors to a specific client, call: `client.Node.Intercept(...)`.
func (c *Client) Intercept(interceptors ...Interceptor) {
c.User.Intercept(interceptors...)
}
// Mutate implements the ent.Mutator interface.
func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
switch m := m.(type) {
@@ -152,6 +158,12 @@ func (c *UserClient) Use(hooks ...Hook) {
c.hooks.User = append(c.hooks.User, hooks...)
}
// Use adds a list of query interceptors to the interceptors stack.
// A call to `Intercept(f, g, h)` equals to `user.Intercept(f(g(h())))`.
func (c *UserClient) Intercept(interceptors ...Interceptor) {
c.inters.User = append(c.inters.User, interceptors...)
}
// Create returns a builder for creating a User entity.
func (c *UserClient) Create() *UserCreate {
mutation := newUserMutation(c.config, OpCreate)
@@ -204,6 +216,7 @@ func (c *UserClient) DeleteOneID(id int) *UserDeleteOne {
func (c *UserClient) Query() *UserQuery {
return &UserQuery{
config: c.config,
inters: c.Interceptors(),
}
}
@@ -226,6 +239,11 @@ func (c *UserClient) Hooks() []Hook {
return c.hooks.User
}
// Interceptors returns the client interceptors.
func (c *UserClient) Interceptors() []Interceptor {
return c.inters.User
}
func (c *UserClient) mutate(ctx context.Context, m *UserMutation) (Value, error) {
switch m.Op() {
case OpCreate:

View File

@@ -24,12 +24,19 @@ type config struct {
log func(...any)
// hooks to execute on mutations.
hooks *hooks
// interceptors to execute on queries.
inters *inters
}
// hooks per client, for fast access.
type hooks struct {
User []ent.Hook
}
// hooks and interceptors per client, for fast access.
type (
hooks struct {
User []ent.Hook
}
inters struct {
User []ent.Interceptor
}
)
// Options applies the options on the config object.
func (c *config) options(opts ...Option) {

View File

@@ -10,6 +10,7 @@ import (
"context"
"errors"
"fmt"
"reflect"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
@@ -19,14 +20,20 @@ import (
// ent aliases to avoid import conflicts in user's code.
type (
Op = ent.Op
Hook = ent.Hook
Value = ent.Value
Query = ent.Query
Policy = ent.Policy
Mutator = ent.Mutator
Mutation = ent.Mutation
MutateFunc = ent.MutateFunc
Op = ent.Op
Hook = ent.Hook
Value = ent.Value
Query = ent.Query
Querier = ent.Querier
QuerierFunc = ent.QuerierFunc
Interceptor = ent.Interceptor
InterceptFunc = ent.InterceptFunc
Traverser = ent.Traverser
TraverseFunc = ent.TraverseFunc
Policy = ent.Policy
Mutator = ent.Mutator
Mutation = ent.Mutation
MutateFunc = ent.MutateFunc
)
// OrderFunc applies an ordering on the sql selector.
@@ -466,5 +473,86 @@ func (s *selector) BoolX(ctx context.Context) bool {
return v
}
// newQueryContext returns a new context with the given QueryContext attached in case it does not exist.
func newQueryContext(ctx context.Context, typ, op string) context.Context {
if ent.QueryFromContext(ctx) == nil {
ctx = ent.NewQueryContext(ctx, &ent.QueryContext{Type: typ, Op: op})
}
return ctx
}
func querierAll[V Value, Q interface {
sqlAll(context.Context, ...queryHook) (V, error)
}]() Querier {
return QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
query, ok := q.(Q)
if !ok {
return nil, fmt.Errorf("unexpected query type %T", q)
}
return query.sqlAll(ctx)
})
}
func querierCount[Q interface {
sqlCount(context.Context) (int, error)
}]() Querier {
return QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
query, ok := q.(Q)
if !ok {
return nil, fmt.Errorf("unexpected query type %T", q)
}
return query.sqlCount(ctx)
})
}
func withInterceptors[V Value](ctx context.Context, q Query, qr Querier, inters []Interceptor) (v V, err error) {
for i := len(inters) - 1; i >= 0; i-- {
qr = inters[i].Intercept(qr)
}
rv, err := qr.Query(ctx, q)
if err != nil {
return v, err
}
vt, ok := rv.(V)
if !ok {
return v, fmt.Errorf("unexpected type %T returned from %T. expected type: %T", vt, q, v)
}
return vt, nil
}
func scanWithInterceptors[Q1 ent.Query, Q2 interface {
sqlScan(context.Context, Q1, any) error
}](ctx context.Context, rootQuery Q1, selectOrGroup Q2, inters []Interceptor, v any) error {
rv := reflect.ValueOf(v)
var qr Querier = QuerierFunc(func(ctx context.Context, q Query) (Value, error) {
query, ok := q.(Q1)
if !ok {
return nil, fmt.Errorf("unexpected query type %T", q)
}
if err := selectOrGroup.sqlScan(ctx, query, v); err != nil {
return nil, err
}
if k := rv.Kind(); k == reflect.Pointer && rv.Elem().CanInterface() {
return rv.Elem().Interface(), nil
}
return v, nil
})
for i := len(inters) - 1; i >= 0; i-- {
qr = inters[i].Intercept(qr)
}
vv, err := qr.Query(ctx, rootQuery)
if err != nil {
return err
}
switch rv2 := reflect.ValueOf(vv); {
case rv.IsNil(), rv2.IsNil(), rv.Kind() != reflect.Pointer:
case rv.Type() == rv2.Type():
rv.Elem().Set(rv2.Elem())
case rv.Elem().Type() == rv2.Type():
rv.Elem().Set(rv2)
}
return nil
}
// queryHook describes an internal hook for the different sqlAll methods.
type queryHook func(context.Context, *sqlgraph.QuerySpec)

View File

@@ -19,11 +19,10 @@ type UserFunc func(context.Context, *ent.UserMutation) (ent.Value, error)
// Mutate calls f(ctx, m).
func (f UserFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
mv, ok := m.(*ent.UserMutation)
if !ok {
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m)
if mv, ok := m.(*ent.UserMutation); ok {
return f(ctx, mv)
}
return f(ctx, mv)
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.UserMutation", m)
}
// Condition is a hook condition function.

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"sync"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/examples/migration/ent/predicate"
"entgo.io/ent/examples/migration/ent/user"
@@ -307,6 +308,16 @@ func (m *UserMutation) Where(ps ...predicate.User) {
m.predicates = append(m.predicates, ps...)
}
// WhereP appends storage-level predicates to the UserMutation builder. Using this method,
// users can use type-assertion to append predicates that do not depend on any generated package.
func (m *UserMutation) WhereP(ps ...func(*sql.Selector)) {
p := make([]predicate.User, len(ps))
for i := range ps {
p[i] = ps[i]
}
m.Where(p...)
}
// Op returns the operation name.
func (m *UserMutation) Op() Op {
return m.op

View File

@@ -55,9 +55,15 @@ func (ud *UserDelete) Exec(ctx context.Context) (int, error) {
}
mut = ud.hooks[i](mut)
}
if _, err := mut.Mutate(ctx, ud.mutation); err != nil {
n, err := mut.Mutate(ctx, ud.mutation)
if err != nil {
return 0, err
}
nv, ok := n.(int)
if !ok {
return 0, fmt.Errorf("unexpected type %T returned from mutation. expected type: int", n)
}
affected = nv
}
return affected, err
}

View File

@@ -26,6 +26,7 @@ type UserQuery struct {
unique *bool
order []OrderFunc
fields []string
inters []Interceptor
predicates []predicate.User
// intermediate query (i.e. traversal path).
sql *sql.Selector
@@ -38,13 +39,13 @@ func (uq *UserQuery) Where(ps ...predicate.User) *UserQuery {
return uq
}
// Limit adds a limit step to the query.
// Limit the number of records to be returned by this query.
func (uq *UserQuery) Limit(limit int) *UserQuery {
uq.limit = &limit
return uq
}
// Offset adds an offset step to the query.
// Offset to start from.
func (uq *UserQuery) Offset(offset int) *UserQuery {
uq.offset = &offset
return uq
@@ -57,7 +58,7 @@ func (uq *UserQuery) Unique(unique bool) *UserQuery {
return uq
}
// Order adds an order step to the query.
// Order specifies how the records should be ordered.
func (uq *UserQuery) Order(o ...OrderFunc) *UserQuery {
uq.order = append(uq.order, o...)
return uq
@@ -66,7 +67,7 @@ func (uq *UserQuery) Order(o ...OrderFunc) *UserQuery {
// First returns the first User entity from the query.
// Returns a *NotFoundError when no User was found.
func (uq *UserQuery) First(ctx context.Context) (*User, error) {
nodes, err := uq.Limit(1).All(ctx)
nodes, err := uq.Limit(1).All(newQueryContext(ctx, TypeUser, "First"))
if err != nil {
return nil, err
}
@@ -89,7 +90,7 @@ func (uq *UserQuery) FirstX(ctx context.Context) *User {
// Returns a *NotFoundError when no User ID was found.
func (uq *UserQuery) FirstID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = uq.Limit(1).IDs(ctx); err != nil {
if ids, err = uq.Limit(1).IDs(newQueryContext(ctx, TypeUser, "FirstID")); err != nil {
return
}
if len(ids) == 0 {
@@ -112,7 +113,7 @@ func (uq *UserQuery) FirstIDX(ctx context.Context) int {
// Returns a *NotSingularError when more than one User entity is found.
// Returns a *NotFoundError when no User entities are found.
func (uq *UserQuery) Only(ctx context.Context) (*User, error) {
nodes, err := uq.Limit(2).All(ctx)
nodes, err := uq.Limit(2).All(newQueryContext(ctx, TypeUser, "Only"))
if err != nil {
return nil, err
}
@@ -140,7 +141,7 @@ func (uq *UserQuery) OnlyX(ctx context.Context) *User {
// Returns a *NotFoundError when no entities are found.
func (uq *UserQuery) OnlyID(ctx context.Context) (id int, err error) {
var ids []int
if ids, err = uq.Limit(2).IDs(ctx); err != nil {
if ids, err = uq.Limit(2).IDs(newQueryContext(ctx, TypeUser, "OnlyID")); err != nil {
return
}
switch len(ids) {
@@ -165,10 +166,12 @@ func (uq *UserQuery) OnlyIDX(ctx context.Context) int {
// All executes the query and returns a list of Users.
func (uq *UserQuery) All(ctx context.Context) ([]*User, error) {
ctx = newQueryContext(ctx, TypeUser, "All")
if err := uq.prepareQuery(ctx); err != nil {
return nil, err
}
return uq.sqlAll(ctx)
qr := querierAll[[]*User, *UserQuery]()
return withInterceptors[[]*User](ctx, uq, qr, uq.inters)
}
// AllX is like All, but panics if an error occurs.
@@ -183,6 +186,7 @@ func (uq *UserQuery) AllX(ctx context.Context) []*User {
// IDs executes the query and returns a list of User IDs.
func (uq *UserQuery) IDs(ctx context.Context) ([]int, error) {
var ids []int
ctx = newQueryContext(ctx, TypeUser, "IDs")
if err := uq.Select(user.FieldID).Scan(ctx, &ids); err != nil {
return nil, err
}
@@ -200,10 +204,11 @@ func (uq *UserQuery) IDsX(ctx context.Context) []int {
// Count returns the count of the given query.
func (uq *UserQuery) Count(ctx context.Context) (int, error) {
ctx = newQueryContext(ctx, TypeUser, "Count")
if err := uq.prepareQuery(ctx); err != nil {
return 0, err
}
return uq.sqlCount(ctx)
return withInterceptors[int](ctx, uq, querierCount[*UserQuery](), uq.inters)
}
// CountX is like Count, but panics if an error occurs.
@@ -217,6 +222,7 @@ func (uq *UserQuery) CountX(ctx context.Context) int {
// Exist returns true if the query has elements in the graph.
func (uq *UserQuery) Exist(ctx context.Context) (bool, error) {
ctx = newQueryContext(ctx, TypeUser, "Exist")
switch _, err := uq.FirstID(ctx); {
case IsNotFound(err):
return false, nil
@@ -270,16 +276,11 @@ func (uq *UserQuery) Clone() *UserQuery {
// Aggregate(ent.Count()).
// Scan(ctx, &v)
func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy {
grbuild := &UserGroupBy{config: uq.config}
grbuild.fields = append([]string{field}, fields...)
grbuild.path = func(ctx context.Context) (prev *sql.Selector, err error) {
if err := uq.prepareQuery(ctx); err != nil {
return nil, err
}
return uq.sqlQuery(ctx), nil
}
uq.fields = append([]string{field}, fields...)
grbuild := &UserGroupBy{build: uq}
grbuild.flds = &uq.fields
grbuild.label = user.Label
grbuild.flds, grbuild.scan = &grbuild.fields, grbuild.Scan
grbuild.scan = grbuild.Scan
return grbuild
}
@@ -297,10 +298,10 @@ func (uq *UserQuery) GroupBy(field string, fields ...string) *UserGroupBy {
// Scan(ctx, &v)
func (uq *UserQuery) Select(fields ...string) *UserSelect {
uq.fields = append(uq.fields, fields...)
selbuild := &UserSelect{UserQuery: uq}
selbuild.label = user.Label
selbuild.flds, selbuild.scan = &uq.fields, selbuild.Scan
return selbuild
sbuild := &UserSelect{UserQuery: uq}
sbuild.label = user.Label
sbuild.flds, sbuild.scan = &uq.fields, sbuild.Scan
return sbuild
}
// Aggregate returns a UserSelect configured with the given aggregations.
@@ -309,6 +310,16 @@ func (uq *UserQuery) Aggregate(fns ...AggregateFunc) *UserSelect {
}
func (uq *UserQuery) prepareQuery(ctx context.Context) error {
for _, inter := range uq.inters {
if inter == nil {
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
}
if trv, ok := inter.(Traverser); ok {
if err := trv.Traverse(ctx, uq); err != nil {
return err
}
}
}
for _, f := range uq.fields {
if !user.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
@@ -440,13 +451,8 @@ func (uq *UserQuery) sqlQuery(ctx context.Context) *sql.Selector {
// UserGroupBy is the group-by builder for User entities.
type UserGroupBy struct {
config
selector
fields []string
fns []AggregateFunc
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
build *UserQuery
}
// Aggregate adds the given aggregation functions to the group-by query.
@@ -455,58 +461,46 @@ func (ugb *UserGroupBy) Aggregate(fns ...AggregateFunc) *UserGroupBy {
return ugb
}
// Scan applies the group-by query and scans the result into the given value.
// Scan applies the selector query and scans the result into the given value.
func (ugb *UserGroupBy) Scan(ctx context.Context, v any) error {
query, err := ugb.path(ctx)
if err != nil {
ctx = newQueryContext(ctx, TypeUser, "GroupBy")
if err := ugb.build.prepareQuery(ctx); err != nil {
return err
}
ugb.sql = query
return ugb.sqlScan(ctx, v)
return scanWithInterceptors[*UserQuery, *UserGroupBy](ctx, ugb.build, ugb, ugb.build.inters, v)
}
func (ugb *UserGroupBy) sqlScan(ctx context.Context, v any) error {
for _, f := range ugb.fields {
if !user.ValidColumn(f) {
return &ValidationError{Name: f, err: fmt.Errorf("invalid field %q for group-by", f)}
}
func (ugb *UserGroupBy) sqlScan(ctx context.Context, root *UserQuery, v any) error {
selector := root.sqlQuery(ctx).Select()
aggregation := make([]string, 0, len(ugb.fns))
for _, fn := range ugb.fns {
aggregation = append(aggregation, fn(selector))
}
selector := ugb.sqlQuery()
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(*ugb.flds)+len(ugb.fns))
for _, f := range *ugb.flds {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
selector.GroupBy(selector.Columns(*ugb.flds...)...)
if err := selector.Err(); err != nil {
return err
}
rows := &sql.Rows{}
query, args := selector.Query()
if err := ugb.driver.Query(ctx, query, args, rows); err != nil {
if err := ugb.build.driver.Query(ctx, query, args, rows); err != nil {
return err
}
defer rows.Close()
return sql.ScanSlice(rows, v)
}
func (ugb *UserGroupBy) sqlQuery() *sql.Selector {
selector := ugb.sql.Select()
aggregation := make([]string, 0, len(ugb.fns))
for _, fn := range ugb.fns {
aggregation = append(aggregation, fn(selector))
}
if len(selector.SelectedColumns()) == 0 {
columns := make([]string, 0, len(ugb.fields)+len(ugb.fns))
for _, f := range ugb.fields {
columns = append(columns, selector.C(f))
}
columns = append(columns, aggregation...)
selector.Select(columns...)
}
return selector.GroupBy(selector.Columns(ugb.fields...)...)
}
// UserSelect is the builder for selecting fields of User entities.
type UserSelect struct {
*UserQuery
selector
// intermediate query (i.e. traversal path).
sql *sql.Selector
}
// Aggregate adds the given aggregation functions to the selector query.
@@ -517,26 +511,27 @@ func (us *UserSelect) Aggregate(fns ...AggregateFunc) *UserSelect {
// Scan applies the selector query and scans the result into the given value.
func (us *UserSelect) Scan(ctx context.Context, v any) error {
ctx = newQueryContext(ctx, TypeUser, "Select")
if err := us.prepareQuery(ctx); err != nil {
return err
}
us.sql = us.UserQuery.sqlQuery(ctx)
return us.sqlScan(ctx, v)
return scanWithInterceptors[*UserQuery, *UserSelect](ctx, us.UserQuery, us, us.inters, v)
}
func (us *UserSelect) sqlScan(ctx context.Context, v any) error {
func (us *UserSelect) sqlScan(ctx context.Context, root *UserQuery, v any) error {
selector := root.sqlQuery(ctx)
aggregation := make([]string, 0, len(us.fns))
for _, fn := range us.fns {
aggregation = append(aggregation, fn(us.sql))
aggregation = append(aggregation, fn(selector))
}
switch n := len(*us.selector.flds); {
case n == 0 && len(aggregation) > 0:
us.sql.Select(aggregation...)
selector.Select(aggregation...)
case n != 0 && len(aggregation) > 0:
us.sql.AppendSelect(aggregation...)
selector.AppendSelect(aggregation...)
}
rows := &sql.Rows{}
query, args := us.sql.Query()
query, args := selector.Query()
if err := us.driver.Query(ctx, query, args, rows); err != nil {
return err
}