Rename ArgumentValue to FieldValue
Prior to this commit, `ArgumentValue<T>` would mainly focus on the server-side support with the binding of arguments on Controller methods. With the introduction of this feature on the client in gh-1174, this commit reconsiders both the `ArgumentValue<T>` name and its package location to reflect the broader support. This commit deprecates `ArgumentValue<T>` in favor of `FieldValue<T>` with similar support. Closes gh-1187
This commit is contained in:
@@ -143,11 +143,11 @@ See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
|
||||
|
||||
See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
|
||||
|
||||
| `ArgumentValue`
|
||||
| `FieldValue`
|
||||
| For access to a named field argument bound to a higher-level, typed Object along
|
||||
with a flag to indicate if the input argument was omitted vs set to `null`.
|
||||
|
||||
See xref:controllers.adoc#controllers.schema-mapping.argument-value[`ArgumentValue`].
|
||||
See xref:controllers.adoc#controllers.schema-mapping.field-value[`FieldValue`].
|
||||
|
||||
| `@Arguments`
|
||||
| For access to all field arguments bound to a higher-level, typed Object.
|
||||
@@ -403,8 +403,8 @@ specified in the annotation, or to the parameter name. For access to the full ar
|
||||
map, please use xref:controllers.adoc#controllers.schema-mapping.arguments[`@Arguments`] instead.
|
||||
|
||||
|
||||
[[controllers.schema-mapping.argument-value]]
|
||||
=== `ArgumentValue`
|
||||
[[controllers.schema-mapping.field-value]]
|
||||
=== `FieldValue`
|
||||
|
||||
By default, input arguments in GraphQL are nullable and optional, which means an argument
|
||||
can be set to the `null` literal, or not provided at all. This distinction is useful for
|
||||
@@ -414,7 +414,7 @@ there is no way to make such a distinction, because you would get `null` or an e
|
||||
`Optional` in both cases.
|
||||
|
||||
If you want to know not whether a value was not provided at all, you can declare an
|
||||
`ArgumentValue` method parameter, which is a simple container for the resulting value,
|
||||
`FieldValue` method parameter, which is a simple container for the resulting value,
|
||||
along with a flag to indicate whether the input argument was omitted altogether. You
|
||||
can use this instead of `@Argument`, in which case the argument name is determined from
|
||||
the method parameter name, or together with `@Argument` to specify the argument name.
|
||||
@@ -427,7 +427,7 @@ For example:
|
||||
public class BookController {
|
||||
|
||||
@MutationMapping
|
||||
public void addBook(ArgumentValue<BookInput> bookInput) {
|
||||
public void addBook(FieldValue<BookInput> bookInput) {
|
||||
if (!bookInput.isOmitted()) {
|
||||
BookInput value = bookInput.value();
|
||||
// ...
|
||||
@@ -436,7 +436,7 @@ For example:
|
||||
}
|
||||
----
|
||||
|
||||
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
|
||||
`FieldValue` is also supported as a field within the object structure of an `@Argument`
|
||||
method parameter, either initialized via a constructor argument or via a setter, including
|
||||
as a field of an object nested at any level below the top level object.
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Simple container for GraphQL field values that indicates if the field
|
||||
* value is present, provided but set to {@literal "null"} or omitted altogether.
|
||||
*
|
||||
* <p>In the case of GraphQL mutations, clients send an Input Object with several fields;
|
||||
* the server must understand the difference between a field being absent
|
||||
* (the existing value should be left as-is) and a field set to {@literal "null"}
|
||||
* (the existing value must be set to {@literal "null"}). {@code FieldValue<T>}
|
||||
* helps to make this distinction.
|
||||
*
|
||||
* <p>Supported in one of the following places:
|
||||
* <ul>
|
||||
* <li>On a controller method parameter, either instead of
|
||||
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
|
||||
* in which case the argument name is determined from the method parameter name,
|
||||
* or together with {@code @Argument} to specify the argument name.
|
||||
* <li>As a field within the object structure of an {@code @Argument} method
|
||||
* parameter, either initialized via a constructor argument or a setter,
|
||||
* including as a field of an object nested at any level below the top level
|
||||
* object.
|
||||
* </ul>
|
||||
*
|
||||
* @param <T> the type of value contained
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.4.0
|
||||
* @see <a href="http://spec.graphql.org/October2021/#sec-Input-Objects">Input Object</a>
|
||||
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
|
||||
*/
|
||||
public final class FieldValue<T> {
|
||||
|
||||
private static final FieldValue<?> EMPTY = new FieldValue<>(null, false);
|
||||
|
||||
private static final FieldValue<?> OMITTED = new FieldValue<>(null, true);
|
||||
|
||||
|
||||
@Nullable
|
||||
private final T value;
|
||||
|
||||
private final boolean omitted;
|
||||
|
||||
|
||||
private FieldValue(@Nullable T value, boolean omitted) {
|
||||
this.value = value;
|
||||
this.omitted = omitted;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return {@code true} if a non-null value is present, and {@code false} otherwise.
|
||||
*/
|
||||
public boolean isPresent() {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the input value was present in the input but the value was {@code null},
|
||||
* and {@code false} otherwise.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return !this.omitted && this.value == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the input value was omitted altogether from the
|
||||
* input, and {@code false} if it was provided, but possibly set to the
|
||||
* {@literal "null"} literal.
|
||||
*/
|
||||
public boolean isOmitted() {
|
||||
return this.omitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contained value, or {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public T value() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contained value as a nullable {@link Optional}.
|
||||
*/
|
||||
public Optional<T> asOptional() {
|
||||
return Optional.ofNullable(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a value is present, performs the given action with the value, otherwise does nothing.
|
||||
* @param action the action to be performed, if a value is present
|
||||
*/
|
||||
public void ifPresent(Consumer<? super T> action) {
|
||||
Assert.notNull(action, "Action is required");
|
||||
if (this.value != null) {
|
||||
action.accept(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
// This covers EMPTY and OMITTED constant
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof FieldValue<?> otherValue)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(this.value, otherValue.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = ObjectUtils.nullSafeHashCode(this.value);
|
||||
result = 31 * result + Boolean.hashCode(this.omitted);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (this.isOmitted()) {
|
||||
return "FieldValue{omitted}";
|
||||
}
|
||||
return "FieldValue{value=" + this.value + "'}'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method for an argument value that was provided, even if
|
||||
* it was set to {@literal "null}.
|
||||
* @param <T> the type of value
|
||||
* @param value the value to hold in the instance
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> FieldValue<T> ofNullable(@Nullable T value) {
|
||||
return (value != null) ? new FieldValue<>(value, false) : (FieldValue<T>) EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method for an argument value that was omitted.
|
||||
* @param <T> the type of value
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> FieldValue<T> omitted() {
|
||||
return (FieldValue<T>) OMITTED;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,10 +18,8 @@ package org.springframework.graphql.data;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -46,7 +44,9 @@ import org.springframework.util.ObjectUtils;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.1.0
|
||||
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
|
||||
* @deprecated since 1.4.0 in favor of {@link org.springframework.graphql.FieldValue}.
|
||||
*/
|
||||
@Deprecated(since = "1.4.0", forRemoval = true)
|
||||
public final class ArgumentValue<T> {
|
||||
|
||||
private static final ArgumentValue<?> EMPTY = new ArgumentValue<>(null, false);
|
||||
@@ -73,15 +73,6 @@ public final class ArgumentValue<T> {
|
||||
return (this.value != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the input value was present in the input but the value was {@code null},
|
||||
* and {@code false} otherwise.
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return !this.omitted && this.value == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the input value was omitted altogether from the
|
||||
* input, and {@code false} if it was provided, but possibly set to the
|
||||
@@ -106,18 +97,6 @@ public final class ArgumentValue<T> {
|
||||
return Optional.ofNullable(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a value is present, performs the given action with the value, otherwise does nothing.
|
||||
* @param action the action to be performed, if a value is present
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public void ifPresent(Consumer<? super T> action) {
|
||||
Assert.notNull(action, "Action is required");
|
||||
if (this.value != null) {
|
||||
action.accept(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
// This covers EMPTY and OMITTED constant
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -67,13 +68,15 @@ import org.springframework.validation.FieldError;
|
||||
*
|
||||
* <p>The binder supports {@link Optional} as a wrapper around any Object or
|
||||
* scalar value in the target Object structure. In addition, it also supports
|
||||
* {@link ArgumentValue} as a wrapper that indicates whether a given input
|
||||
* argument was omitted rather than set to the {@literal "null"} literal.
|
||||
* {@link org.springframework.graphql.FieldValue} as a wrapper that indicates
|
||||
* whether a given input argument was omitted rather than set to the
|
||||
* {@literal "null"} literal.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public class GraphQlArgumentBinder {
|
||||
|
||||
@Nullable
|
||||
@@ -115,7 +118,7 @@ public class GraphQlArgumentBinder {
|
||||
* @param name the name of an argument, or {@code null} to use the full map
|
||||
* @param targetType the type of Object to create
|
||||
* @return the created Object, possibly wrapped in {@link Optional} or in
|
||||
* {@link ArgumentValue}, or {@code null} if there is no value
|
||||
* {@link org.springframework.graphql.FieldValue}, or {@code null} if there is no value
|
||||
* @throws BindException containing one or more accumulated errors from
|
||||
* matching and/or converting arguments to the target Object
|
||||
*/
|
||||
@@ -175,8 +178,9 @@ public class GraphQlArgumentBinder {
|
||||
|
||||
boolean isOptional = (targetClass == Optional.class);
|
||||
boolean isArgumentValue = (targetClass == ArgumentValue.class);
|
||||
boolean isFieldValue = (targetClass == FieldValue.class);
|
||||
|
||||
if (isOptional || isArgumentValue) {
|
||||
if (isOptional || isArgumentValue || isFieldValue) {
|
||||
targetType = targetType.getNested(2);
|
||||
targetClass = targetType.resolve();
|
||||
}
|
||||
@@ -202,6 +206,9 @@ public class GraphQlArgumentBinder {
|
||||
else if (isArgumentValue) {
|
||||
value = (isOmitted ? ArgumentValue.omitted() : ArgumentValue.ofNullable(value));
|
||||
}
|
||||
else if (isFieldValue) {
|
||||
value = (isOmitted ? FieldValue.omitted() : FieldValue.ofNullable(value));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
@@ -488,10 +489,12 @@ public class AnnotatedControllerConfigurer
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public Map<String, ResolvableType> getArguments() {
|
||||
|
||||
Predicate<MethodParameter> argumentPredicate = (p) ->
|
||||
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class);
|
||||
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class ||
|
||||
p.getParameterType() == FieldValue.class);
|
||||
|
||||
return Arrays.stream(this.mappingInfo.getHandlerMethod().getMethodParameters())
|
||||
.filter(argumentPredicate)
|
||||
|
||||
@@ -20,6 +20,7 @@ import graphql.schema.DataFetchingEnvironment;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
@@ -37,12 +38,13 @@ import org.springframework.validation.BindException;
|
||||
* parameter type.
|
||||
*
|
||||
* <p>This resolver also supports wrapping the target object with
|
||||
* {@link ArgumentValue} if the application wants to differentiate between an
|
||||
* input argument that was set to {@code null} vs not provided at all. When
|
||||
* this wrapper type is used, the annotation is optional, and the name of the
|
||||
* argument is derived from the method parameter name.
|
||||
* {@link FieldValue} if the application
|
||||
* wants to differentiate between an input argument that was set to
|
||||
* {@code null} vs not provided at all.
|
||||
* When this wrapper type is used, the annotation is optional,
|
||||
* and the name of the argument is derived from the method parameter name.
|
||||
*
|
||||
* <p>An {@link ArgumentValue} can also be nested within the object structure
|
||||
* <p>An {@link FieldValue} can also be nested within the object structure
|
||||
* of an {@link Argument @Argument}-annotated method parameter.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -50,6 +52,7 @@ import org.springframework.validation.BindException;
|
||||
* @since 1.0.0
|
||||
* @see org.springframework.graphql.data.method.annotation.support.ArgumentsMethodArgumentResolver
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final GraphQlArgumentBinder argumentBinder;
|
||||
@@ -72,7 +75,8 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return (parameter.getParameterAnnotation(Argument.class) != null ||
|
||||
parameter.getParameterType() == ArgumentValue.class);
|
||||
parameter.getParameterType() == ArgumentValue.class ||
|
||||
parameter.getParameterType() == FieldValue.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,9 +107,10 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
|
||||
return argument.name();
|
||||
}
|
||||
}
|
||||
else if (parameter.getParameterType() != ArgumentValue.class) {
|
||||
else if (parameter.getParameterType() != ArgumentValue.class &&
|
||||
parameter.getParameterType() != FieldValue.class) {
|
||||
throw new IllegalStateException(
|
||||
"Expected either @Argument or a method parameter of type ArgumentValue");
|
||||
"Expected either @Argument or a method parameter of type FieldValue");
|
||||
}
|
||||
|
||||
String parameterName = parameter.getParameterName();
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.graphql.data.ArgumentValue;
|
||||
* @since 1.2.2
|
||||
*/
|
||||
@UnwrapByDefault
|
||||
@SuppressWarnings("removal")
|
||||
public final class ArgumentValueValueExtractor implements ValueExtractor<ArgumentValue<@ExtractedValue ?>> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.data.method.annotation.support;
|
||||
|
||||
import jakarta.validation.valueextraction.ExtractedValue;
|
||||
import jakarta.validation.valueextraction.UnwrapByDefault;
|
||||
import jakarta.validation.valueextraction.ValueExtractor;
|
||||
|
||||
import org.springframework.graphql.FieldValue;
|
||||
|
||||
/**
|
||||
* {@link ValueExtractor} that enables {@code @Valid} with {@link FieldValue},
|
||||
* and helps to extract the value from it.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.4.0
|
||||
*/
|
||||
@UnwrapByDefault
|
||||
public final class FieldValueValueExtractor implements ValueExtractor<FieldValue<@ExtractedValue ?>> {
|
||||
|
||||
@Override
|
||||
public void extractValues(FieldValue<?> fieldValue, ValueReceiver receiver) {
|
||||
if (!fieldValue.isOmitted()) {
|
||||
receiver.value(null, fieldValue.value());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.web.ProjectedPayload;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
@@ -97,13 +98,16 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
|
||||
return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private static Class<?> getTargetType(MethodParameter parameter) {
|
||||
Class<?> type = parameter.getParameterType();
|
||||
return (type.equals(Optional.class) || type.equals(ArgumentValue.class)) ?
|
||||
return (type.equals(Optional.class) || type.equals(ArgumentValue.class) ||
|
||||
type.equals(FieldValue.class)) ?
|
||||
parameter.nested().getNestedParameterType() : parameter.getParameterType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
|
||||
|
||||
String name = (parameter.hasParameterAnnotation(Argument.class) ?
|
||||
@@ -112,8 +116,9 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
|
||||
Class<?> targetType = parameter.getParameterType();
|
||||
boolean isOptional = (targetType == Optional.class);
|
||||
boolean isArgumentValue = (targetType == ArgumentValue.class);
|
||||
boolean isFieldValue = (targetType == FieldValue.class);
|
||||
|
||||
if (isOptional || isArgumentValue) {
|
||||
if (isOptional || isArgumentValue || isFieldValue) {
|
||||
targetType = parameter.nested().getNestedParameterType();
|
||||
}
|
||||
|
||||
@@ -128,6 +133,10 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
|
||||
return (name != null && arguments.containsKey(name)) ?
|
||||
ArgumentValue.ofNullable(value) : ArgumentValue.omitted();
|
||||
}
|
||||
else if (isFieldValue) {
|
||||
return (name != null && arguments.containsKey(name)) ?
|
||||
FieldValue.ofNullable(value) : FieldValue.omitted();
|
||||
}
|
||||
else {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.projection.TargetAware;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
|
||||
@@ -245,9 +246,11 @@ class SchemaMappingBeanFactoryInitializationAotProcessor implements BeanFactoryI
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public void apply(RuntimeHints runtimeHints) {
|
||||
Type parameterType = this.methodParameter.getGenericParameterType();
|
||||
if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType())) {
|
||||
if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType()) ||
|
||||
FieldValue.class.isAssignableFrom(this.methodParameter.getParameterType())) {
|
||||
parameterType = this.methodParameter.nested().getNestedGenericParameterType();
|
||||
}
|
||||
bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), parameterType);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
org.springframework.graphql.data.method.annotation.support.FieldValueValueExtractor
|
||||
org.springframework.graphql.data.method.annotation.support.ArgumentValueValueExtractor
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.data;
|
||||
package org.springframework.graphql;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -23,14 +23,14 @@ import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ArgumentValue}.
|
||||
* Tests for {@link FieldValue}.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ArgumentValueTests {
|
||||
class FieldValueTests {
|
||||
|
||||
@Test
|
||||
void existingValueShouldBePresent() {
|
||||
ArgumentValue<String> message = ArgumentValue.ofNullable("hello");
|
||||
FieldValue<String> message = FieldValue.ofNullable("hello");
|
||||
assertThat(message.isOmitted()).isFalse();
|
||||
assertThat(message.isPresent()).isTrue();
|
||||
assertThat(message.isEmpty()).isFalse();
|
||||
@@ -38,7 +38,7 @@ class ArgumentValueTests {
|
||||
|
||||
@Test
|
||||
void nullValueShouldBePresent() {
|
||||
ArgumentValue<String> message = ArgumentValue.ofNullable(null);
|
||||
FieldValue<String> message = FieldValue.ofNullable(null);
|
||||
assertThat(message.isOmitted()).isFalse();
|
||||
assertThat(message.isPresent()).isFalse();
|
||||
assertThat(message.isEmpty()).isTrue();
|
||||
@@ -46,7 +46,7 @@ class ArgumentValueTests {
|
||||
|
||||
@Test
|
||||
void noValueShouldBeOmitted() {
|
||||
ArgumentValue<String> message = ArgumentValue.omitted();
|
||||
FieldValue<String> message = FieldValue.omitted();
|
||||
assertThat(message.isOmitted()).isTrue();
|
||||
assertThat(message.isPresent()).isFalse();
|
||||
assertThat(message.isEmpty()).isFalse();
|
||||
@@ -54,29 +54,29 @@ class ArgumentValueTests {
|
||||
|
||||
@Test
|
||||
void asOptionalShouldMapOmitted() {
|
||||
assertThat(ArgumentValue.omitted().asOptional()).isEmpty();
|
||||
assertThat(ArgumentValue.ofNullable(null).asOptional()).isEmpty();
|
||||
assertThat(ArgumentValue.ofNullable("hello").asOptional()).isPresent();
|
||||
assertThat(FieldValue.omitted().asOptional()).isEmpty();
|
||||
assertThat(FieldValue.ofNullable(null).asOptional()).isEmpty();
|
||||
assertThat(FieldValue.ofNullable("hello").asOptional()).isPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ifPresentShouldExecuteWhenValue() {
|
||||
AtomicBoolean called = new AtomicBoolean();
|
||||
ArgumentValue.ofNullable("hello").ifPresent(value -> called.set(true));
|
||||
FieldValue.ofNullable("hello").ifPresent(value -> called.set(true));
|
||||
assertThat(called.get()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ifPresentShouldSkipWhenNull() {
|
||||
AtomicBoolean called = new AtomicBoolean();
|
||||
ArgumentValue.ofNullable(null).ifPresent(value -> called.set(true));
|
||||
FieldValue.ofNullable(null).ifPresent(value -> called.set(true));
|
||||
assertThat(called.get()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ifPresentShouldSkipWhenOmitted() {
|
||||
AtomicBoolean called = new AtomicBoolean();
|
||||
ArgumentValue.omitted().ifPresent(value -> called.set(true));
|
||||
FieldValue.omitted().ifPresent(value -> called.set(true));
|
||||
assertThat(called.get()).isFalse();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,6 +38,7 @@ import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.BindException;
|
||||
@@ -53,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
* @author Brian Clozel
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class GraphQlArgumentBinderTests {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
@@ -248,6 +250,30 @@ class GraphQlArgumentBinderTests {
|
||||
assertThat(itemBean.getName().isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithOptionalFieldBeanArgument() throws Exception {
|
||||
|
||||
ResolvableType targetType =
|
||||
ResolvableType.forClass(PrimaryConstructorOptionalFieldItemBean.class);
|
||||
|
||||
Object result = bind(
|
||||
"{\"item\":{\"name\":\"Item name\",\"age\":\"30\"},\"name\":\"Hello\"}", targetType);
|
||||
|
||||
assertThat(result).isInstanceOf(PrimaryConstructorOptionalFieldItemBean.class).isNotNull();
|
||||
PrimaryConstructorOptionalFieldItemBean itemBean = (PrimaryConstructorOptionalFieldItemBean) result;
|
||||
|
||||
assertThat(itemBean.getItem().value().getName()).isEqualTo("Item name");
|
||||
assertThat(itemBean.getItem().value().getAge()).isEqualTo(30);
|
||||
assertThat(itemBean.getName().value()).isEqualTo("Hello");
|
||||
|
||||
result = bind("{\"key\":{}}", targetType);
|
||||
itemBean = (PrimaryConstructorOptionalFieldItemBean) result;
|
||||
|
||||
assertThat(itemBean).isNotNull();
|
||||
assertThat(itemBean.getItem().isOmitted()).isTrue();
|
||||
assertThat(itemBean.getName().isPresent()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void primaryConstructorWithNestedBeanList() throws Exception {
|
||||
|
||||
@@ -570,6 +596,26 @@ class GraphQlArgumentBinderTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class PrimaryConstructorOptionalFieldItemBean {
|
||||
|
||||
private final FieldValue<String> name;
|
||||
|
||||
private final FieldValue<Item> item;
|
||||
|
||||
public PrimaryConstructorOptionalFieldItemBean(FieldValue<String> name, FieldValue<Item> item) {
|
||||
this.name = name;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public FieldValue<String> getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public FieldValue<Item> getItem() {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class NoPrimaryConstructorBean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
@@ -53,6 +54,9 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
param = methodParam(BookController.class, "addBook", ArgumentValue.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isTrue();
|
||||
|
||||
param = methodParam(BookController.class, "addBook", FieldValue.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isTrue();
|
||||
|
||||
param = methodParam(BookController.class, "rawArgumentValue", Map.class);
|
||||
assertThat(this.resolver.supportsParameter(param)).isTrue();
|
||||
|
||||
@@ -81,7 +85,7 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveJavaBeanArgumentWithWrapper() throws Exception {
|
||||
void shouldResolveJavaBeanArgumentWithArgumentWrapper() throws Exception {
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "addBook", ArgumentValue.class),
|
||||
environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"));
|
||||
@@ -94,6 +98,20 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
.hasFieldOrPropertyWithValue("authorId", 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveJavaBeanArgumentWithFieldWrapper() throws Exception {
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "addBook", FieldValue.class),
|
||||
environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"));
|
||||
|
||||
assertThat(result)
|
||||
.isNotNull()
|
||||
.isInstanceOf(FieldValue.class)
|
||||
.extracting(value -> ((FieldValue<?>) value).value())
|
||||
.hasFieldOrPropertyWithValue("name", "test name")
|
||||
.hasFieldOrPropertyWithValue("authorId", 42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveListOfJavaBeansArgument() throws Exception {
|
||||
Object result = this.resolver.resolveArgument(
|
||||
@@ -152,6 +170,11 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public Book addBook(FieldValue<BookInput> bookInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@MutationMapping
|
||||
public List<Book> addBooks(@Argument List<Book> books) {
|
||||
return null;
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.GraphQlArgumentBinder;
|
||||
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.graphql.data.method.annotation.Arguments;
|
||||
@@ -109,15 +109,15 @@ class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport {
|
||||
@SuppressWarnings({"NotNullFieldNotInitialized", "unused"})
|
||||
static class BookInput {
|
||||
|
||||
ArgumentValue<String> name;
|
||||
FieldValue<String> name;
|
||||
|
||||
Long authorId;
|
||||
|
||||
public ArgumentValue<String> getName() {
|
||||
public FieldValue<String> getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(ArgumentValue<String> name) {
|
||||
public void setName(FieldValue<String> name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.web.ProjectedPayload;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@@ -54,7 +54,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
|
||||
testSupports("projection", BookProjection.class, true);
|
||||
testSupports("optionalProjection", Optional.class, true);
|
||||
testSupports("optionalString", Optional.class, false);
|
||||
testSupports("argumentValueProjection", ArgumentValue.class, true);
|
||||
testSupports("fieldValueProjection", FieldValue.class, true);
|
||||
}
|
||||
|
||||
void testSupports(String methodName, Class<?> methodParamType, boolean supported) {
|
||||
@@ -86,39 +86,39 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
|
||||
}
|
||||
|
||||
@Test
|
||||
void argumentValuePresent() throws Exception {
|
||||
void fieldValuePresent() throws Exception {
|
||||
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
|
||||
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
|
||||
environment("{ \"where\" : { \"author\" : \"Orwell\" }}"));
|
||||
|
||||
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
|
||||
BookProjection book = ((ArgumentValue<BookProjection>) result).value();
|
||||
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
|
||||
BookProjection book = ((FieldValue<BookProjection>) result).value();
|
||||
assertThat(book.getAuthor()).isEqualTo("Orwell");
|
||||
}
|
||||
|
||||
@Test
|
||||
void argumentValueSetToNull() throws Exception {
|
||||
void fieldValueSetToNull() throws Exception {
|
||||
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
|
||||
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
|
||||
environment("{ \"where\" : null}"));
|
||||
|
||||
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
|
||||
ArgumentValue<BookProjection> value = ((ArgumentValue<BookProjection>) result);
|
||||
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
|
||||
FieldValue<BookProjection> value = ((FieldValue<BookProjection>) result);
|
||||
assertThat(value.isPresent()).isFalse();
|
||||
assertThat(value.isOmitted()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void argumentValueIsOmitted() throws Exception {
|
||||
void fieldValueIsOmitted() throws Exception {
|
||||
|
||||
Object result = this.resolver.resolveArgument(
|
||||
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
|
||||
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
|
||||
environment("{}"));
|
||||
|
||||
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
|
||||
ArgumentValue<BookProjection> value = ((ArgumentValue<BookProjection>) result);
|
||||
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
|
||||
FieldValue<BookProjection> value = ((FieldValue<BookProjection>) result);
|
||||
assertThat(value.isPresent()).isFalse();
|
||||
assertThat(value.isOmitted()).isTrue();
|
||||
}
|
||||
@@ -153,7 +153,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
|
||||
}
|
||||
|
||||
@QueryMapping
|
||||
public List<Book> argumentValueProjection(@Argument(name = "where") ArgumentValue<BookProjection> projection) {
|
||||
public List<Book> fieldValueProjection(@Argument(name = "where") FieldValue<BookProjection> projection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ import org.springframework.data.projection.TargetAware;
|
||||
import org.springframework.data.web.ProjectedPayload;
|
||||
import org.springframework.graphql.Author;
|
||||
import org.springframework.graphql.Book;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.federation.EntityMapping;
|
||||
import org.springframework.graphql.data.method.annotation.Argument;
|
||||
import org.springframework.graphql.data.method.annotation.BatchMapping;
|
||||
@@ -120,12 +120,12 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerBindingReflectionOnArgumentValue() {
|
||||
processBeanClasses(ArgumentValueController.class);
|
||||
assertThatIntrospectionOnMethodsHintRegisteredForType(ArgumentValueController.class);
|
||||
assertThatInvocationHintRegisteredForMethods(ArgumentValueController.class, "addBook");
|
||||
void registerBindingReflectionOnFieldValue() {
|
||||
processBeanClasses(FieldValueController.class);
|
||||
assertThatIntrospectionOnMethodsHintRegisteredForType(FieldValueController.class);
|
||||
assertThatInvocationHintRegisteredForMethods(FieldValueController.class, "addBook");
|
||||
assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, BookInput.class);
|
||||
assertThatHintsAreNotRegisteredForTypes(ArgumentValue.class);
|
||||
assertThatHintsAreNotRegisteredForTypes(FieldValue.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,9 +173,9 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests {
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class ArgumentValueController {
|
||||
static class FieldValueController {
|
||||
@MutationMapping
|
||||
public Book addBook(ArgumentValue<BookInput> bookInput) {
|
||||
public Book addBook(FieldValue<BookInput> bookInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.assertj.core.api.ThrowableAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.graphql.data.ArgumentValue;
|
||||
import org.springframework.graphql.FieldValue;
|
||||
import org.springframework.graphql.data.method.HandlerMethod;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@@ -71,20 +71,20 @@ class ValidationHelperTests {
|
||||
BiConsumer<Object, Object[]> validator2 = validateFunction(MyBean.class, "myValidatedParameterMethod");
|
||||
assertViolation(() -> validator2.accept(bean, new Object[] {new ConstrainedInput(100)}), "integerValue");
|
||||
|
||||
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidArgumentValue");
|
||||
assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable("")}), "myValidArgumentValue.arg0");
|
||||
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidFieldValue");
|
||||
assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable("")}), "myValidFieldValue.arg0");
|
||||
|
||||
// Validate that an explicit null value is validated.
|
||||
assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable(null)}), "myValidArgumentValue.arg0");
|
||||
assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable(null)}), "myValidFieldValue.arg0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRaiseValidationErrorForOmittedArgumentValue() {
|
||||
void shouldNotRaiseValidationErrorForOmittedFieldValue() {
|
||||
MyBean bean = new MyBean();
|
||||
|
||||
// Validate that an omitted value is allowed.
|
||||
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidArgumentValue");
|
||||
validator3.accept(bean, new Object[] {ArgumentValue.omitted()});
|
||||
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidFieldValue");
|
||||
validator3.accept(bean, new Object[] {FieldValue.omitted()});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,7 +173,7 @@ class ValidationHelperTests {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Object myValidArgumentValue(@Valid ArgumentValue<@NotBlank String> arg0) {
|
||||
public Object myValidFieldValue(@Valid FieldValue<@NotBlank String> arg0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user