opened
denisahearn
username_0
After upgrading from graphql 1.11.6 to graphql 1.12.1, a custom `field` instrumentation class that my app defines stopped getting called during query and mutation execution.

The custom field implementation is shown below, and is used to convert the value of any `String` field that is empty (e.g. `''`) to `null` in the response. It is based on this suggestion: https://github.com/username_1/graphql-ruby/issues/807#issuecomment-310423259

```
# For any fields defined as types.String convert an empty string to null.
class ApplyNullIfEmptyString
  def instrument(_type, field)
    if field.type == GraphQL::STRING_TYPE
      # Get the resolve proc from the field and wrap it with your logic:
      new_resolve = EnsureString.new(field.resolve_proc)

      # If you use graphql-batch to load any strings, you'll need this too:
      new_lazy_resolve = EnsureString.new(field.lazy_resolve_proc)

      # Make a copy of the field which has the new behavior
      field.redefine(resolve: new_resolve, lazy_resolve: new_lazy_resolve)
    else
      # return the field unchanged
      field
    end
  end
end

class EnsureString
  def initialize(inner_resolve)
    @inner_resolve = inner_resolve
  end

  def call(obj, args, ctx)
    result = @inner_resolve.call(obj, args, ctx)
    if result == ''
      # convert empty strings to null
      result = nil
    end
    result
  end
end
```

And then in the schema.rb file:

```
class Schema < GraphQL::Schema
  ...
  instrument(:field, ApplyNullIfEmptyString.new)
  ...
end
```

**Versions**

`graphql` version: 1.12.1
`rails` (or other framework): 6.0.3.4
other applicable versions (`graphql-batch`, etc)

**GraphQL schema**

```ruby
class User < GraphQL::Schema::Object
  description 'A user within the system.'

  field :id, ID, null: false, description: 'The ID of the user.'
  field :first_name, String, null: false, description: 'The first name of the user.'
  field :last_name, String, null: true, description: 'The last name of the user.'
  field :email, String, null: false, description: 'The email address for the user.'
end

class Query < GraphQL::Schema::Object
  field :user, resolver: User
end

class Schema < GraphQL::Schema
  query Sentera::Types::Query
  instrument(:field, ApplyNullIfEmptyString.new)
end

class User < GraphQL::Schema::Resolver
  type User, null: false
  description 'The current user.'

  def resolve
    context[:current_user]
  end
end
```

**GraphQL query**

```graphql
query FetchCurrentUser {
  user {
    id
    first_name
    last_name
    email
  }
}```

```json
{
  "data": {
    "user": {
      "id": 5,
      "first_name": "John",
      "last_name": "",
      "email": "john.doe@example.com"
    }
  }
}```

**Steps to reproduce**

Execute the above query for a user record that has an empty `last_name` in the database.

**Expected behavior**

The expected behavior is that the custom instrumentation should convert the empty last name to `null`, with `null` returned in the response.

**Actual behavior**

The custom instrumentation class is no longer getting called during query and mutation execution/resolution. So instead of a `null` getting returned in the response for `String` fields that are empty, an empty string is being returned.

**Additional context**

After upgrading, I noticed that the following deprecation warning is now getting emitted during query execution:

"Field instrumentation (Sentera::FieldInstrumentation::ApplyNullIfEmptyString) will be removed in GraphQL-Ruby 2.0, please upgrade to field extensions: https://graphql-ruby.org/type_definitions/field_extensions.html"

The problem with this suggestion is that I want the conversion of `'' -> null` behavior to be applied consistently across all `String` fields in my GraphQL schema. I don't want to have to add a field extension to each of our String fields to get this behavior, because remembering to add this extension could easily be overlooked the next time a `String` field is added to the schema.

I found https://github.com/username_1/graphql-ruby/issues/2087, which hints towards using tracing to solve this problem, however it's not clear to me from the online documentation of how to achieve this.

Since field instrumentation is eventually going away, I would prefer to find an alternative, centralized solution for this problem. Is it possible to achieve what I'm after using the tracing feature?
Field instrumentation no longer working
issue
created
rmosolgo
username_1
Hi! I think an extension would work here. For example: 

```ruby 
class EnsureString < GraphQL::Schema::FieldExtension 
  def after_resolve(value:, **_rest)
    value || ""
  end 
end 
class Types::BaseField < GraphQL::Schema::Field 
  def initialize(**kwargs, &block)
    super 
    # TODO: use `type.unwrap == ...` if you want to match `String!` too 
    if type == GraphQL::Types::String
      extension(EnsureString)
    end 
  end 
end 
```

In that case, the field class would _always_ add an extension whenever it's configured to return `String`. 

Thanks for sharing that example; here's an update to it, using the approach I described above: 

<details>
<summary>Ensuring strings with field extensions</summary>

<p>

```ruby 
require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "graphql", "1.12.1"
end

class EnsureString < GraphQL::Schema::FieldExtension
  def after_resolve(value:, **_rest)
    value || ""
  end
end

class BaseField < GraphQL::Schema::Field
  def initialize(*args, **kwargs, &block)
    super
    # TODO: use `type.unwrap` to also match `String, null: false`
    if type == GraphQL::Types::String
      extension(EnsureString)
    end
  end
end

class BaseObject < GraphQL::Schema::Object
  field_class(BaseField)
end

class User < BaseObject
  field :first_name, String, null: false, description: 'The first name of the user.'
  field :last_name, String, null: true, description: 'The last name of the user.'
end

class Query < BaseObject
  field :user, User, null: true

  def user
    { first_name: "Plato", last_name: nil }
  end
end

class Schema < GraphQL::Schema
  query Query
end

pp Schema.execute(<<-GRAPHQL).to_h
query FetchCurrentUser {
  user {
    firstName
    lastName
  }
}
GRAPHQL
# => {"data"=>{"user"=>{"firstName"=>"Plato", "lastName"=>""}}}
```

</p>
</details>

How does that look? Give a try and let me know how it goes!
comment
created
denisahearn
username_0
Hi Robert. Thanks so much for the quick support! 

Your suggestion was spot on and easy to implement. In fact, our app already had a `BaseField` class that is plugged into `BaseType` and `BaseInterface` classes, so all I had to do was create the new field extension class, and plug it in via the existing `BaseField` class. That fixed the problem with our broken specs, and our GraphQL API is now back up and running as expected.

Denis
comment