Skip to content

Conversation

a8m
Copy link
Member

@a8m a8m commented Feb 29, 2020

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:

  • Documentation.
  • Align Gremlin dialect with the new mutation interface.
  • Create edge constant for interacting with the mutation interface.
  • Load hooks from schema - There's one open question here - if the schema imports the
    generated ent package, we can have a circular dependency situation (because ent.Client needs to register its schema.Hooks, but can't import them). So, we need to decide on one solution here.
    1. Generate a generated.go file in the user's schema package, that will register its hooks in the generated ent package. Then, users will need to empty import it as follows:
      import (
          "<project>/ent"
          _ "<project>/ent/schema"
      )
      // ... 
    2. We'll create a new entclient package, and request users to use it in order to create a new
      ent.Client.
      import (
          "<project>/entclient"
      )
      
      func main() {
          client, err := entclient.Open("...")
          // ...
      }
    The 2 new packages will read the hooks from the schema, and register them in the 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:

  • Add package hook (also WIP) for auto type-assertion, or operation filtering. For example:
    hk := func(next ent.Mutator) ent.Mutator {
        return hook.CardCreate(func(ctx context.Context, m *ent.CardMutation) (ent.Value, error) {
            // boring.
            return next.Mutate(ctx, m)
        })
    }
  • Add the option for loading mutated nodes (or getting old values of fields).

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 29, 2020
@a8m a8m force-pushed the hooks/initial branch 12 times, most recently from 140dd95 to 5063f87 Compare March 3, 2020 14:28
@a8m
Copy link
Member Author

a8m commented Mar 4, 2020

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 ent.Mutation definition is:

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:

// ID returns the id value in the mutation. Note that, the id
// is available only if it was provided to the builder.
func (m *CardMutation) ID() (id int, exist bool)

// SetBoring sets the boring field.
func (m *CardMutation) SetBoring(t time.Time)

// Boring returns the boring value in the mutation.
func (m *CardMutation) Boring() (r time.Time, exist bool)

// ResetBoring reset all changes of the boring field.
func (m *CardMutation) ResetBoring()

// SetNumber sets the number field.
func (m *CardMutation) SetNumber(s string)

// Number returns the number value in the mutation.
func (m *CardMutation) Number() (r string, exist bool) 

// ResetNumber reset all changes of the number field.
func (m *CardMutation) ResetNumber()

// SetName sets the name field.
func (m *CardMutation) SetName(s string)

// Name returns the name value in the mutation.
func (m *CardMutation) Name() (r string, exist bool) 

// ClearName clears the value of name.
func (m *CardMutation) ClearName()

// NameCleared returns if the field name was cleared in this mutation.
func (m *CardMutation) NameCleared() bool

// ResetName reset all changes of the name field.
func (m *CardMutation) ResetName() 

// AddFriendIDs adds the friends edge to Card by ids.
func (m *CardMutation) AddFriendIDs(ids ...int)

// RemoveFriendIDs removes the friends edge to Card by ids.
func (m *CardMutation) RemoveFriendIDs(ids ...int) 

// RemovedFriends returns the removed ids of friends.
func (m *CardMutation) RemovedFriendsIDs() (ids []int)

// FriendsIDs returns the friends ids in the mutation.
func (m *CardMutation) FriendsIDs() (ids []int)

// ResetFriends reset all changes of the friends edge.
func (m *CardMutation) ResetFriends()

// SetBestFriendID sets the best_friend edge to Card by id.
func (m *CardMutation) SetBestFriendID(id int)

// ClearBestFriend clears the best_friend edge to Card.
func (m *CardMutation) ClearBestFriend() 

// BestFriendCleared returns if the edge best_friend was cleared.
func (m *CardMutation) BestFriendCleared() bool 

// BestFriendIDs returns the best_friend ids in the mutation.
func (m *CardMutation) BestFriendIDs() (ids []int)

// ResetBestFriend reset all changes of the best_friend edge.
func (m *CardMutation) ResetBestFriend()

When dealing with generic mutations, there are 2 options to read and mutate fields/edges
of multiple types. One, use the Mutation methods:

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 hook package for auto type-assertion:

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 ent.Client/ent.Tx and loading old values.

@a8m a8m force-pushed the hooks/initial branch from 5063f87 to 51fb881 Compare March 4, 2020 08:54
@a8m a8m mentioned this pull request Mar 4, 2020
@a8m a8m force-pushed the hooks/initial branch from 51fb881 to 4b60c3c Compare March 4, 2020 16:06
@alexsn alexsn closed this Mar 5, 2020
@alexsn alexsn deleted the hooks/initial branch March 5, 2020 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants