GraphQL SPQR расширяет параметры мутации входного объекта
Способность расширять существующие типы является блестящей, потому что она позволяет модулировать код и разделять привилегии. Я нашел хорошие примеры того, как расширить вывод объектов в запросе (см. Ниже), но нет хорошего способа расширить входные данные данного объекта.
Для примера скажем, у нас есть класс User
,
class User {
String firstName;
String lastName;
}
Если мы объявим bean-компонент, у нас может быть такой запрос:
/**
* This is valid and can be invoked using
* query {
* user(id=1) {
* firstName
* lastName
* }
* }
*/
@GraphQLQuery(name = "user")
public User getUser(@GraphQLArgument(name = "id") long id) {
}
Затем в другом бобе мы можем расширить пользователя
/**
* <<this currently works>>
* So now this query becomes valid
* query {
* user(id=1) {
* firstName
* lastName
* address { <-- this is not a top level, but extends User
* streetNam
* }
* }
* }
*/
@GraphQLQuery(name = "address")
public Address getUserAddress(@GraphQLContext User) {
}
Аналогично для мутации мы можем определить:
/**
* <<this currently works>>
* This can be invoked using:
* mutation {
* addUser(user :{
* firstName: "John"
* lastName: "Smith"
* })
* fistName
* }
*/
@GraphQLMutation(name = "addUser")
public User addUser(@GraphQLArgument(name = "user") User user) {
}
Сейчас пытаюсь добавить address
Таким же образом мы добавили его для запроса, но добавили входной аргумент User
, Следующее все еще объявлено в некотором бобе.
/**
* << this is what I am trying to achieve>>
* I want to be able to invoke the following query and not having to declare 'Address' inside of 'User' class.
* mutation {
* addUser(user :{
* firstName: "John"
* lastName: "Smith"
* address: { <-- being able to pass address as argument now, and be part of user.
* streetName: "1 str"
* }
* })
* fistName
* }
*/
// e.g. something like ...
@GraphQLInputField(name = "address")
public void addAddressToUser(@GraphQLContext User user, @GraphQLArgument Address address) {
}
1 ответ
Я придумал способ, которым вы можете сделать это в настоящее время, но это требует определенных усилий.
Я использую GraphQL-SPQR 0.9.8 (который я выпущу через несколько дней). Вы можете добиться того же в 0.9.7, это немного менее эргономично.
@Test
public void testSchema() {
GraphQLSchema schema = new GraphQLSchemaGenerator()
// Ignore extra fields (like "address") when deserializing
.withValueMapperFactory(JacksonValueMapperFactory.builder()
.withConfigurers(conf -> conf.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false))
.build())
.withInputFieldBuilders((conf, defaults) -> defaults.prepend(new UserInputBuilder(defaults.getFirstOfType(JacksonValueMapper.class))))
.withArgumentInjectors(new UserInjector())
.withOperationsFromSingleton(new TestService())
.generate();
GraphQL exe = GraphQL.newGraphQL(schema).build();
ExecutionResult result = exe.execute("{user(in: {name: \"A. Man\", address: {type: \"home\", street: {name: \"Fakestreet\", number: 123}}}) {name, street {number}}}");
}
public class TestService {
@GraphQLQuery //or mutation, no difference
public User user(User in) {
return in;
}
}
// Redefines how User objects are deserizalized
public static class UserInjector extends InputValueDeserializer {
@Override
public Object getArgumentValue(ArgumentInjectorParams params) {
User user = (User) super.getArgumentValue(params);
Map<?, ?> rawInput = (Map<?, ?>) params.getInput();
Address address = params.getResolutionEnvironment().valueMapper.fromInput(rawInput.get("address"), GenericTypeReflector.annotate(Address.class));
// Preprocess the address in any way you need, here I just extract the street
user.setStreet(address.getStreet());
return user;
}
@Override
public boolean supports(AnnotatedType type) {
return GenericTypeReflector.isSuperType(User.class, type.getType());
}
}
//Redefines the way User input type is mapped
public static class UserInputBuilder implements InputFieldBuilder {
private final InputFieldBuilder original;
public UserInputBuilder(InputFieldBuilder original) {
this.original = original;
}
@Override
public Set<InputField> getInputFields(InputFieldBuilderParams params) {
Set<InputField> fields = original.getInputFields(params);
// Add the extra "address" field you want
fields.add(new InputField("address", "User's home address", GenericTypeReflector.annotate(Address.class), null, null));
return fields;
}
@Override
public boolean supports(AnnotatedType type) {
return GenericTypeReflector.isSuperType(User.class, type.getType());
}
}
public class User {
private String name;
private Street street;
public User(String name, Street street) {
this.name = name;
this.street = street;
}
public String getName() {
return name;
}
public Street getStreet() {
return street;
}
@GraphQLIgnore //can be filtered out in a different way, without touching this class
public void setStreet(Street street) {
this.street = street;
}
}
public class Address {
private Street street;
private String type;
public Address(Street street, String type) {
this.street = street;
this.type = type;
}
public Street getStreet() {
return street;
}
public String getType() {
return type;
}
}
Тем не менее, может быть, было бы проще просто зарегистрировать пользовательский десериализатор в Джексоне (или Gson, что бы вы ни использовали) и пропустить пользовательский ArgumentInjector
,