Skip to content
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

Add Back includeIfNullOnInput build options #339

Open
yuhangang opened this issue Mar 1, 2024 · 12 comments
Open

Add Back includeIfNullOnInput build options #339

yuhangang opened this issue Mar 1, 2024 · 12 comments
Labels
enhancement New feature or request planned

Comments

@yuhangang
Copy link

We planned to migrate to Graphql Codegen from Artemis, a discontinued project to using GraphQL options.

For query/mutations variables, GraphQL Codegen will remove all fields with NULL value during processing, this introduce risks and workload for our backend engineers to migrate while maintaining the behaviour for older releases and our website. Therefore, We would like to see that includeIfNullOnInput build options is available again.

Thanks you for your consideration.

Copy link

github-actions bot commented Mar 1, 2024

👋 @yuhangang
Thank you for raising an issue. I will investigate the issue and get back to you as soon as possible.
Please make sure you have provided enough context.

This library is created and maintained by me, @budde377. Please consider supporting my work and ensure our survival by donating here.

@budde377
Copy link
Contributor

budde377 commented Mar 1, 2024

So just to make sure I understand your request.

You are migrating your code base and would like a way to declare input where null values are sent to the server instead of being omitted.

You can declare null values to be sent using the copyWith as demonstrated here, but I recon that is too verbose?

@yuhangang
Copy link
Author

yuhangang commented Mar 1, 2024

yup, u get my point, sry for my bad English.

I considered to declaring to adding copyWith for each query/mutation. But it would be good to have a consistent and predictable way to sending null params to server.

@budde377
Copy link
Contributor

budde377 commented Mar 1, 2024 via email

@yuhangang
Copy link
Author

Yup, that would be more predictable compared to manual handling for each fields

@keithcwk
Copy link

keithcwk commented Mar 5, 2024

@budde377 Just curious, instead of an alternative such as Input.withNull, is it possible to reinstate/implement a build option similar to includeIfNullOnInput?

@budde377
Copy link
Contributor

budde377 commented Mar 5, 2024

Everything is possible 🌈

Why do you prefer this option?

@keithcwk
Copy link

keithcwk commented Mar 6, 2024

Think it benefits project where there are multiple query/mutation arguments that allows null values, as it omits the need to manually add the withNull operator at multiple parts of the code

@budde377 budde377 added enhancement New feature or request planned labels Mar 22, 2024
@Kaelten
Copy link

Kaelten commented Nov 8, 2024

Having a better solution for this would be preferable! Have you considered trying to use sentinel values? I'm not sure how viable that is though.

@budde377
Copy link
Contributor

budde377 commented Nov 8, 2024

@Kaelten, can you give me an example of what you had in mind?

@Kaelten
Copy link

Kaelten commented Nov 10, 2024

@Kaelten, can you give me an example of what you had in mind?

Note I'm fairly new to dart, so I'm not sure how crazy of an idea this actually is. I did come up with a few options but they all have draw backs and tradeoffs. I think there are other riffs on these options but they all seem to come back to some version of the following:

Optional Wrappers

In this version we have to wrap valid values with an object, not great, but it maintains perfect type safety.

class Optional<T> {
  final T? value;
  final bool isUndefined;

  Optional(this.value) : isUndefined = false {}

  const Optional.undefined()
      : value = null,
        isUndefined = true;
}

class Input$ProfileInput {
  final Optional<String?> displayName;
  final Optional<int?> age;
  final Optional<String?> email;

  Input$ProfileInput({
    Optional<String?> displayName = const Optional.undefined(),
    Optional<int?> age = const Optional.undefined(),
    Optional<String?> email = const Optional.undefined(),
  })  : this.displayName = displayName,
        this.age = age,
        this.email = email;

  Map<String, Object?> toMap() {
    return {
      if (!this.displayName.isUndefined) 'displayName': displayName.value,
      if (!this.age.isUndefined) 'age': age.value,
      if (!this.email.isUndefined) 'email': email.value,
    };
  }
}

void main() {
  var input = Input$ProfileInput(
    displayName: Optional('John Doe'),
    email: Optional(null),
  );

  print(input.toMap());
}

Leveraging an Object constant

This method avoids having to wrap every value with an object, but it looses compile time type safety on the constructor while maintaining it within the class at least.

class _Undefined {
  const _Undefined();

  static const instance = _Undefined();
}

class Input$ProfileInput {
  final String? displayName;
  final int? age;
  final String? email;
  final Set<String> _definedFields;

  Input$ProfileInput({
    Object? displayName = _Undefined.instance,
    Object? age = _Undefined.instance,
    Object? email = _Undefined.instance,
  })  : this.displayName =
            displayName == _Undefined.instance ? null : displayName as String?,
        this.age = age == _Undefined.instance ? null : age as int?,
        this.email = email == _Undefined.instance ? null : email as String?,
        _definedFields = {
          if (displayName != _Undefined.instance) 'displayName',
          if (age != _Undefined.instance) 'age',
          if (email != _Undefined.instance) 'email',
        };

  Map<String, Object?> toMap() {
    return {
      if (_definedFields.contains('displayName')) 'displayName': displayName,
      if (_definedFields.contains('age')) 'age': age,
      if (_definedFields.contains('email')) 'email': email,
    };
  }
}

void main() {
  var input = Input$ProfileInput(
    displayName: 'John Doe',
    email: null,
  );

  print(input.toMap());
}

Magic Values

This version relies on magic constants to represent undefined values.

On the plus side we have perfect type saftey without any cruft, but the con is that we have to have a valid value for all possible fields. In some cases, you might be able to make use of null, but in others config options could be used to specify sentinel values in case negative max int or similar is in fact a valid value.

class Input$ProfileInput {
  final String? displayName;
  final int? age;
  final String? email;

  static const _undefinedString = r'Input$ProfileInput$__undefined_string';
  static const _undefinedInt = -(1 << 53); // max neg int on dart web

  Input$ProfileInput({
    String? displayName = _undefinedString,
    int? age = _undefinedInt,
    String? email = _undefinedString,
  })  : this.displayName = displayName,
        this.age = age,
        this.email = email;

  Map<String, Object?> toMap() {
    return {
      if (displayName != _undefinedString) 'displayName': displayName,
      if (age != _undefinedInt) 'age': age,
      if (email != _undefinedString) 'email': email,
    };
  }
}

@budde377
Copy link
Contributor

budde377 commented Nov 11, 2024

Thanks, @Kaelten - these are good, well-presented suggestions.

I have added a variation of version two, which follows how the copyWith is currently implemented.

When the configuration is enabled by setting EXPERIMENTAL_enable_input_builders: true in the config and upgrading to version 1.1.0, the input constructor takes a function that takes a builder function as its first argument and returns an instance of the input.

Given the schema

input Foo {

  requiredValue: String!
  optionalValue1: String
  optionalValue2: String
  optionalValue3: String
}

the generated class will be

final input = Input$Foo((builder) => builder(
  requiredValue: "value a",
  optionalValue1: "value b",
  optionalValue2: null
));

and the generated JSON will be

{ "requiredValue": "value a", "optionalValue1": "value b", "optionalValue2": null}

Please take if for a spin and let me know how it works out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request planned
Projects
None yet
Development

No branches or pull requests

4 participants