Skip to content

Best practice looking ahead input args of sub types/nodes in resolver? #297

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

Closed
mfn opened this issue Jun 28, 2018 · 3 comments
Closed

Best practice looking ahead input args of sub types/nodes in resolver? #297

mfn opened this issue Jun 28, 2018 · 3 comments

Comments

@mfn
Copy link
Contributor

mfn commented Jun 28, 2018

Example query:

{
  outer {
    inner(input1: "value1) {
      moreFields

Usually in the resolver you receive ResolveInfo $info and thanks to \GraphQL\Type\Definition\ResolveInfo::getFieldSelection you can get a simply representation of the requested structure:

[
  'outer' => [
    'inner' => [
      'moreFields' => true,
…

But to write efficient resolvers it may be necessary to look ahead when resolving outer to know what input args apply to inner, not just the fields.

I've helped myself with copying and modifying `foldSelectionSet` so it returns a structure which contains input args too:
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Type\Definition\ResolveInfo;

/**
 * This adapts \GraphQL\Type\Definition\ResolveInfo::getFieldSelection but with support for arguments
 */
class ResolveFieldArgumentsUtil
{
    /** @var ResolveInfo */
    public $info;

    public function __construct(ResolveInfo $info)
    {
        $this->info = $info;
    }

    /**
     * Helper method that returns names of all fields with attributes selected in query for
     * $this->fieldName up to $depth levels
     *
     * Example:
     * query MyQuery{
     * {
     *   root {
     *     nested(input:value) {
     *      nested1
     *      nested2 {
     *        nested3
     *      }
     *     }
     *   }
     * }
     *
     * Given this ResolveInfo instance is a part of "root" field resolution, and $depth === 1,
     * method will return:
     * [
     *      'nested' => [
     *          'args' => [
     *              'input' => 'value',
     *          ],
     *          'fields' => [
     *              'nested1' => [
     *                  'args' => [],
     *                  'fields' => [],
     *              ],
     *              'nested2' => [
     *                  'args' => [],
     *                  'fields' => [],
     *              ],
     *          ],
     *      ],
     * ],
     *
     * Warning: this method it is a naive implementation which does not take into account
     * conditional typed fragments. So use it with care for fields of interface and union types.
     *
     * @see \GraphQL\Type\Definition\ResolveInfo::getFieldSelection
     * @param int $depth How many levels to include in output
     * @return array
     */
    public function getFieldSelection(int $depth = 0): array
    {
        $fields = [];

        foreach ($this->info->fieldNodes as $fieldNode) {
            $fields = array_merge_recursive($fields, $this->foldSelectionSet($fieldNode->selectionSet, $depth));
        }

        return $fields;
    }

    /**
     * @see \GraphQL\Type\Definition\ResolveInfo::foldSelectionSet
     * @param SelectionSetNode $selectionSet
     * @param int $descend
     * @return array
     */
    private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array
    {
        $fields = [];

        foreach ($selectionSet->selections as $selectionNode) {
            if ($selectionNode instanceof FieldNode) {
                // Only this part here has been adapted to return arguments too
                $name = $selectionNode->name->value;

                $fields[$name] = [
                    'args' => [],
                    'fields' => [],
                ];

                if ($descend > 0 && !empty($selectionNode->selectionSet)) {
                    $fields[$name]['fields'] = $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1);
                }

                foreach ($selectionNode->arguments ?? [] as $argumentNode) {
                    $fields[$name]['args'][$argumentNode->name->value] = $argumentNode->value->value;
                }
            } elseif ($selectionNode instanceof FragmentSpreadNode) {
                $spreadName = $selectionNode->name->value;
                if (isset($this->info->fragments[$spreadName])) {
                    $fragment = $this->info->fragments[$spreadName];
                    $fields = array_merge_recursive($this->foldSelectionSet($fragment->selectionSet, $descend), $fields);
                }
            } elseif ($selectionNode instanceof InlineFragmentNode) {
                $fields = array_merge_recursive($this->foldSelectionSet($selectionNode->selectionSet, $descend), $fields);
            }
        }

        return $fields;
    }
}
[
  'outer' => [
    'args' => [],
    'fields' => [
      'inner' => [
        'args' => ['input1' => 'value1'],
        'fields' => [
          'moreFields' => [
            'input' => [],
            'fields' => [],
…

It gets the job done but doesn't feel exactly right.

Are there any recommendations and best practices to achieve this?

@vladar
Copy link
Member

vladar commented Jul 7, 2018

Best practice is not to look ahead %) It is rather fragile. But if you really need to, then scanning AST as you do is the only way right now.

There is #65 with an idea of a better API for this. PRs are welcome!

@mfn
Copy link
Contributor Author

mfn commented Jul 7, 2018

Thanks for the answer, explains everything I wanted to know! 👍

@mfn mfn closed this as completed Jul 7, 2018
@Konamiman
Copy link

If that helps, this is what I'm using, it will return an array of field information that has the same format as the one returned by ResolveInfo::getFieldSelection but for inner fields having arguments an extra _args key is added to the array of inner fields:

public static function get_fields_info(ResolveInfo $info) {
    return self::get_fields_info_core($info->lookAhead()->queryPlan());
}

private static function get_fields_info_core(array $fields) {
    $result = [];

    foreach($fields as $key => $value) {
        if(empty($value['fields']) && empty($value['args'])) {
            $result[$key] = true;
            continue;
        }

        if(!empty($value['args'])) {
            $result[$key]['_args'] = $value['args'];
        }

        if(!empty($value['fields'])) {
            $result[$key] = array_merge($result[$key], self::get_fields_info_core($value['fields']));
        }
    }

    return $result;
}

So for the original example:

{
  outer {
    inner(input1: "value1) {
      moreFields
  }
}

it will return:

[
  'outer' => [
    'inner' => [
      'moreFields' => true,
      '_args' => [
        'input1 => 'value1'
      ]
    ]
  ]
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants