99designs / gqlgen

go generate based graphql server library
https://gqlgen.com
MIT License
9.98k stars 1.17k forks source link

Support an inline resolver function to access function closure #3129

Open clayne11 opened 5 months ago

clayne11 commented 5 months ago

What happened?

Today, it's possible to annotate a field with @goModel(forceResolver: true) which will generate a resolver for that field. The issue I have is that I often need to pass a good amount of context between the "parent" resolver for the top level type the "child" resolver for the field. Sometimes this context also isn't serializable, and I don't want to expose it into the schema.

Creating child resolvers for specific fields is valuable because sometimes these child fields are very expensive and we want to decouple them from being fetched together, either to support @defer, or simply in case that field isn't actually being requested.

What did you expect?

I would like to propose a mechanism where we can force an inline resolver — instead of creating a new top level entity resolver, instead a closure is generated on the parent model which the server can then call. The closure enables us to pass arbitrary context between the parent and child resolvers.

Example

GraphQL schema:

type Query {
    listing(id: ID): Listing
}

type Listing {
    id: ID!
    name: String
    address: String 
         # "inlineResolver" instead of "forceResolver"
    price: Float! @goField(inlineResolver: true)
}

Generated Go model:

type Listing struct {
    ID      string  `json:"id"`
    Name    *string `json:"name,omitempty"`
    Address *string `json:"address,omitempty"`
    Price   func (context.Context) (float64, error)
}

Population in resolver:

type queryResolver struct{ *Resolver }

func (r *queryResolver) Listing(
    ctx context.Context,
    id *string,
) (*model.Listing, error) {
    listing, err := r.listingController.GetListing(ctx, id)
    if err != nil {
        return nil, err
    }

    return &model.Listing{
        ID:      listing.ID,
        Name:    listing.Name,
        Address: listing.Address,
        Price: func(ctx context.Context) (float64, error) {
            return r.listingService.GetPrice(
                ctx, 
                listing.ID, 
                // accesses internal field we don't want to expose through the schema
                listing.InternalPriceModifier,
            ), nil
        },
    }, nil
}

Once the struct value is returned by the resolver, the server is responsible for executing this inline function, similar to how it would do for a root resolver (only if the Field is actually requested by the consumer).

Alternative

We could also do something like storing properties in OperationContext, but this isn't type safe, can only support serializable data, and also is a lot of manual / error prone work with implicit contracts.

Related to #2864.

StevenACoffman commented 4 months ago

Thanks, I think this is great, and would love to get a PR on it.