-
Notifications
You must be signed in to change notification settings - Fork 975
entc/hooks: initial work for mutations and hooks #377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The usage of hooks in the entity schema looks as follows: type User struct {
ent.Schema
}
func (User) Hooks() []ent.Hook {
return []ent.Hook{
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// m's concrete type is: *ent.UserMutation.
return next.Mutate(ctx, m)
})
},
// More hooks.
// ...
}
} In runtime, you can add global hooks as follows: client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
fmt.Println("mutation start")
defer fmt.Println("mutation end")
return next.Mutate(ctx, m)
})
}) Or, to a specific client: client.User.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
fmt.Println("user mutation start")
defer fmt.Println("user mutation end")
return next.Mutate(ctx, m)
})
}) The type Value interface{}
type Mutation interface {
// Op returns the operation name generated by entc.
Op() string
// Type returns the schema type for this mutation.
Type() string
// Fields returns all fields that were changed during
// this mutation. Note that, in order to get all numeric
// fields that were in/decremented, call AddedFields().
Fields() []string
// Field returns the value of a field with the given name.
// The second boolean value indicates that this field was
// not set, or was not define in the schema.
Field(name string) (Value, bool)
// SetField sets the value for the given name. It returns an
// error if the field is not defined in the schema, or if the
// type mismatch the field type.
SetField(name string, value Value) error
// AddedFields returns all numeric fields that were incremented
// or decremented during this mutation.
AddedFields() []string
// AddedField returns the numeric value that was in/decremented
// from a field with the given name. The second value indicates
// that this field was not set, or was not define in the schema.
AddedField(name string) (Value, bool)
// AddField adds the value for the given name. It returns an
// error if the field is not defined in the schema, or if the
// type mismatch the field type.
AddField(name string, value Value) error
// ClearedFields returns all nullable fields that were cleared
// during this mutation.
ClearedFields() []string
// FieldCleared returns a boolean indicates if this field was
// cleared in this mutation.
FieldCleared(name string) bool
// ClearField clears the value for the given name. It returns an
// error if the field is not defined in the schema.
ClearField(name string) error
// ResetField resets all changes in the mutation regarding the
// given field name. It returns an error if the field is not
// defined in the schema.
ResetField(name string) error
// AddedEdges returns all edge names that were set/added in this
// mutation.
AddedEdges() []string
// AddedIDs returns all ids (to other nodes) that were added for
// the given edge name.
AddedIDs(name string) []Value
// RemovedEdges returns all edge names that were removed in this
// mutation.
RemovedEdges() []string
// RemovedIDs returns all ids (to other nodes) that were removed for
// the given edge name.
RemovedIDs(name string) []Value
// ClearedEdges returns all edge names that were cleared in this
// mutation.
ClearedEdges() []string
// EdgeCleared returns a boolean indicates if this edge was
// cleared in this mutation.
EdgeCleared(name string) bool
// ClearEdge clears the value for the given name. It returns an
// error if the edge name is not defined in the schema.
ClearEdge(name string) error
// ResetEdge resets all changes in the mutation regarding the
// given edge name. It returns an error if the edge is not
// defined in the schema.
ResetEdge(name string) error
} The concrete mutation type gives a type safe API for each schema. For example:
When dealing with generic mutations, there are 2 options to read and mutate fields/edges func M(ctx context.Context, m ent.Mutation) (ent.Value, error) {
v, ok := m.Field(user.FieldName) // or, m.Field("name")
if ok {
// ...
}
// An error is returned when field does not exist in the
// schema, or the value type mismatch the field type.
if err := m.SetField(user.FieldName, "boring"); err != nil {
return nil, err
}
// ...
} Use, type assertion: func M(ctx context.Context, m ent.Mutation) (ent.Value, error) {
type NameSetter interface {
Name() (string, bool)
SetName(string)
}
ns, ok := m.(NameSetter)
if !ok {
// ignore.
}
name, ok := ns.Name()
if ok {
// ...
}
ns.SetName("boring")
// ...
} WIP: An client.Card.Use(func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
// Do something.
return next.Mutate(ctx, m)
})
}) TODO for next PRs: Add an API for getting the |
23823d3
to
fd1879c
Compare
I thought at the start to generate the schema-stitching logic under the user's schema package, but this made the development workflow pretty bad - renaming/deleting schemas require deleting manually the generated file, and build flags didn't help here, because this requires from the user to pass them to all other Go commands (e.g. |
ba577d9
to
b61f1c1
Compare
Per our previous discussion: The first proposal seems the most flexible, and with codegen (i.e. generating the One suggestion for a minor improvement would be to create an interface for each mutation operation (i.e. Here's an adaptation of your example with this suggestion:
Another small benefit is that if the mutation views / results are ever split into separate types, the mutator signature could easily reflect that instead of forcing each mutator to perform a type assertion. For example:
Regardless, excited for this, looks awesome! |
Hey @KCarretto, and thanks for the feedback! We started with a simple implementation (the We don't have a separate interface per operation right now, but we have a hook selector for achieving a similar capability at the moment. For example: hook.On(LoggingHook(), ent.Update|ent.Delete) Thanks for your feedback! |
Documentation will land tomorrow (still on progress). |
#371
This is a WIP and it should be ignored for now.
There are a few things that need to be added before merging it, and additional work that can be added in separate PRs:
generated
ent
package, we can have a circular dependency situation (becauseent.Client
needs to register itsschema.Hooks
, but can't import them). So, we need to decide on one solution here.entschema
, that will register the schema hooks in the generatedent
package. Then, users will need to empty import it as follows:entruntime
or justruntime
instead ofentschema
?entclient
package, and request users to use it in order to create a newent.Client
.ent
package. We keep backwards compatibility, and generate those packages only if the user uses hooks in the schema. No changes are needed, if a user registers hooks on runtime (on client creation).Next steps:
hook
(also WIP) for auto type-assertion, or operation filtering. For example: