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

nested mutation resolvers #189

Closed
crirus opened this issue Oct 18, 2017 · 8 comments
Closed

nested mutation resolvers #189

crirus opened this issue Oct 18, 2017 · 8 comments

Comments

@crirus
Copy link

crirus commented Oct 18, 2017

Hello,

Is there any way to build input objects with own resolvers, so they get executed first then arguments for them get replaced with the resolve return?

Thanks

@vladar
Copy link
Member

vladar commented Oct 19, 2017

I don't think I understand the question. Can you provide some example of what you are trying to achieve?

@crirus
Copy link
Author

crirus commented Oct 19, 2017

Well, I create a mutation with complex types inside, not a flat field set under root types.
Basically I have createUser who's in a new category, for example. Category could be created on it's own resolver then ID passed back to main data structure for user to be created with the category associated.
I can see the root resolver receive all data, but wouldn't be nice to identify all inner types used and if they have a resolver to get called from the deepest up and replace the data node with whatever resolver returns?

@vladar
Copy link
Member

vladar commented Oct 19, 2017

GraphQL is designed with flat mutations at the root. There is some discussion in reference repo and specs about something similar, but nothing like this is supported yet. You may check these repositories for similar questions, maybe you'll find something relevant.

But I may be still missing something from the textual description. Can you post GraphQL mutation example of what you are trying to achieve? E.g.

mutation {
  field1 {
    id # I want this to be passed to field2 args
  }
  field2(id: $id) {
    id
  }
}

@crirus
Copy link
Author

crirus commented Oct 19, 2017

mutation {
   createUser {
      name: "user1"
      createCategory {
          name: "category1"
      }
   }
}

In the end, it will first execute createCategory since I have resolver in CategoryType saving new category, wich return ID:1, then data is passed to createUser resolver with $args['createCategory'] = 1.

Ok, I already resolved all this by splittling the root resolver in calls to resolvers I can find for each type, but would be nice if graphql itself can recursively go deep into each type for a resolver and process the data received for it if any.

@vladar
Copy link
Member

vladar commented Oct 19, 2017

Well, technically you can do something similar by utilizing context (e.g. setting some value in the top resolver and then reading from context in the nested resolver).

But this is against GraphQL design (again, right now specs declare mutations as flat list, no nesting) - this is reflected in the fact that root-level fields of mutation type are resolved sequentially, but all other fields can be resolved by the server in parallel.

See also graphql/graphql-spec#252 and graphql/graphql-js#672

@crirus
Copy link
Author

crirus commented Oct 19, 2017

This is how I resolved it.

//Mutation resolver
public function resolveMutation($val, $args, $context, ResolveInfo $info){
        DebugBreak('1@localhost');
        foreach ($args as $mutationArgName => $mutationData) {//loop in mutations data
            //get the argument object; this data suppoe to represent full object to be saved
            
            $mutationArg = $this->getField($info->fieldName)->getArg($mutationArgName);
            $mutationArgType = $mutationArg->getType();
            
            //particular call to final execution code, eg. save, update, etc. Throws error and halt if not callable found
            $call = $this->getCallableMutator($info->fieldName, $mutationArgType);
            
            //process inner types first, saving as necessary, then save mutation data altered (eg ID instead full inner object data)
            if ($mutationArgType instanceOf BaseInputType){ 
                $args[$mutationArgName] = $mutationArgType->resolveInnerMutation($mutationData);
                //call final outer object save here
            }else{
                //with simple input types, just pass them along to save
            }
            call_user_func($call, $args[$mutationArgName]);
            //return output mutation object
        }
    }

... in OfferInputType class, extending BaseInputType used to define all mutations arguments like

                    'createOffer' => [
                        'name' => 'createOffer',
                        'type' => Types::get('OfferOutput'),
                        'args' => [
                                'offer' => Types::get('OfferInput')
                        ],
public function resolveInnerMutation($inputData){
	$inputDataResolved = [];
	$fields = $this->getFields();
	foreach ($fields as $field) {
	    if(!isset($inputData[$field->name])) continue;//skip where we have no data received
	    $fieldType = $field->getType();
	    if($fieldType instanceOf BaseInputType){//loop into objects and execute the nested resolvers
		if(!empty($inputData['id'])){
		    $inputDataResolved[$field->name] = $inputData['id'];//no creation, just use provided id
		}else{
		    if(is_callable($fieldType->config['resolveField'])){
			$inputDataResolved[$field->name] = call_user_func($fieldType->config['resolveField'], $inputData[$field->name], $field);
		    }else{
			$inputDataResolved[$field->name] = $inputData[$field->name]; //Don't lose data where there are no resolvers defined
		    }
		}
	    }elseif($fieldType instanceOf ListOfType){
		$fieldType = Type::getNamedType($fieldType);
		if($fieldType instanceOf BaseInputType){
		    foreach ($inputData[$field->name] as $key => $inputItem) {
			if(!empty($inputItem['id'])){
			    $inputDataResolved[$field->name][] = $inputItem['id'];//no creation, just use provided id
			}else{                        
			    if(is_callable($fieldType->config['resolveField'])){
				$inputDataResolved[$field->name][] = call_user_func($fieldType->config['resolveField'], $inputItem, $field);
			    }else{
				$inputDataResolved[$field->name][] = $inputItem;//Don't lose data where there are no resolvers defined
			    }
			}
			if(!isset($inputDataResolved[$field->name][$key])) throw new \Exception('Something went wrong processing sub-resolvers; Lost data?');    
		    }
		}else{
		    $inputDataResolved[$field->name] = $inputData[$field->name];
		}
	    }elseif($fieldType instanceOf NonNull){
		$fieldType = Type::getNamedType($fieldType);
		$inputItem = $inputData[$field->name];
		if($fieldType instanceOf BaseInputType){
		    if(!empty($inputItem['id'])){
			$inputDataResolved[$field->name] = $inputItem['id'];//no creation, just use provided id
		    }else{                        
			if(is_callable($fieldType->config['resolveField'])){
			    $inputDataResolved[$field->name] = call_user_func($fieldType->config['resolveField'], $inputItem, $field);
			}else{
			    $inputDataResolved[$field->name] = $inputItem;//Don't lose data where there are no resolvers defined
			}
		    }
		}else{
		    //Set alias where available
		    $inputDataResolved[$field->name] = $inputItem;
		}                
	    }else{
		//field is basic scalar, doesn't need a mutation
		$inputDataResolved[$field->name] = $inputData[$field->name];
	    }
	    if(!isset($inputDataResolved[$field->name])) throw new \Exception('Somethingwent wrong processing sub-resolvers; Lost data?');
	}

	return $inputDataResolved;//Done with this mutation InnerResolvers

}

@vladar
Copy link
Member

vladar commented Oct 21, 2017

So I guess we can close it until some solution emerges in the spec (until then anyone interested can use workarounds listed above).

@AntoniusGolly
Copy link

Is that part of the specs meanwhile? I can't find anything related but this is a pressing issue for me.

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