Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
29 changes: 26 additions & 3 deletions src/main/java/graphql/schema/idl/RuntimeWiring.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public static class Builder {
private final Map<String, SchemaDirectiveWiring> registeredDirectiveWiring = new LinkedHashMap<>();
private final List<SchemaDirectiveWiring> directiveWiring = new ArrayList<>();
private WiringFactory wiringFactory = new NoopWiringFactory();
private boolean strictMode = false;
private boolean strictMode = true;
private GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY;
private GraphQLCodeRegistry codeRegistry = GraphQLCodeRegistry.newCodeRegistry().build();
private GraphqlTypeComparatorRegistry comparatorRegistry = GraphqlTypeComparatorRegistry.AS_IS_REGISTRY;
Expand All @@ -192,11 +192,24 @@ private Builder() {
ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.forEach(this::scalar);
}

/**
* This sets strict mode as true or false. If strictMode is true, if things get defined twice, for example, it will throw a {@link StrictModeWiringException}.
*
* @return this builder
*/
public Builder strictMode(boolean strictMode) {
this.strictMode = strictMode;
return this;
}

/**
* This puts the builder into strict mode, so if things get defined twice, for example, it will throw a {@link StrictModeWiringException}.
*
* @return this builder
*
* @deprecated strictMode default value changed to true, use {@link #strictMode(boolean)} instead
*/
@Deprecated(since = "2025-03-22", forRemoval = true)
public Builder strictMode() {
this.strictMode = true;
return this;
Expand Down Expand Up @@ -300,13 +313,23 @@ public Builder type(String typeName, UnaryOperator<TypeRuntimeWiring.Builder> bu
public Builder type(TypeRuntimeWiring typeRuntimeWiring) {
String typeName = typeRuntimeWiring.getTypeName();
Map<String, DataFetcher> typeDataFetchers = dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>());

Map<String, DataFetcher> additionalFieldDataFetchers = typeRuntimeWiring.getFieldDataFetchers();
if (strictMode && !typeDataFetchers.isEmpty()) {
throw new StrictModeWiringException(format("The type %s has already been defined", typeName));
// Check if the existing type wiring contains overlapping DataFetcher definitions
for (String fieldName : additionalFieldDataFetchers.keySet()) {
if (typeDataFetchers.containsKey(fieldName)) {
throw new StrictModeWiringException(format("The field %s on type %s has already been defined", fieldName, typeName));
}
}
}
typeDataFetchers.putAll(typeRuntimeWiring.getFieldDataFetchers());
typeDataFetchers.putAll(additionalFieldDataFetchers);

DataFetcher<?> defaultDataFetcher = typeRuntimeWiring.getDefaultDataFetcher();
if (defaultDataFetcher != null) {
if (strictMode && defaultDataFetchers.containsKey(typeName)) {
throw new StrictModeWiringException(format("The type %s already has a default data fetcher defined", typeName));
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An edge case that wasn't previously covered: if a type wiring's default datafetcher was redefined at the runtime wiring level (and not the type wiring level), previously didn't raise an error

See test below to lock in the new behaviour

defaultDataFetchers.put(typeName, defaultDataFetcher);
}

Expand Down
23 changes: 18 additions & 5 deletions src/main/java/graphql/schema/idl/TypeRuntimeWiring.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@

/**
* A type runtime wiring is a specification of the data fetchers and possible type resolver for a given type name.
*
* <br>
* This is used by {@link RuntimeWiring} to wire together a functional {@link GraphQLSchema}
*/
@PublicApi
public class TypeRuntimeWiring {

private final static AtomicBoolean DEFAULT_STRICT_MODE = new AtomicBoolean(false);
private final static AtomicBoolean DEFAULT_STRICT_MODE = new AtomicBoolean(true);

/**
* By default {@link TypeRuntimeWiring} builders are not in strict mode, but you can set a JVM wide value
* so that any created will be.
* By default {@link TypeRuntimeWiring} builders are in strict mode, but you can set a JVM wide value too
*
* @param strictMode the desired strict mode state
*
* @see Builder#strictMode()
* @see Builder#strictMode(boolean)
*/
public static void setStrictModeJvmWide(boolean strictMode) {
DEFAULT_STRICT_MODE.set(strictMode);
Expand Down Expand Up @@ -127,6 +126,20 @@ public Builder typeName(String typeName) {
*
* @return this builder
*/
public Builder strictMode(boolean strictMode) {
this.strictMode = strictMode;
return this;
}

/**
* This puts the builder into strict mode, so if things get defined twice, for example, it
* will throw a {@link StrictModeWiringException}.
*
* @return this builder
*
* @deprecated use {@link #strictMode(boolean)} instead
*/
@Deprecated(since = "2025-03-22", forRemoval = true)
public Builder strictMode() {
this.strictMode = true;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import graphql.schema.idl.TypeRuntimeWiring;

/**
* An exception that is throw when {@link RuntimeWiring.Builder#strictMode()} or {@link TypeRuntimeWiring.Builder#strictMode()} is true and
* An exception that is throw when {@link RuntimeWiring.Builder#strictMode(boolean)} or {@link TypeRuntimeWiring.Builder#strictMode(boolean)} is true and
* something gets redefined.
*/
@PublicApi
Expand Down
150 changes: 129 additions & 21 deletions src/test/groovy/graphql/schema/idl/RuntimeWiringTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -193,26 +193,23 @@ class RuntimeWiringTest extends Specification {
newWiring.fieldVisibility == fieldVisibility
}

def "strict mode can stop certain redefinitions"() {
def "strict mode, on by default, can stop certain redefinitions"() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating tests and adding new ones to lock in the change in default behaviour

DataFetcher DF1 = env -> "x"
DataFetcher DF2 = env -> "x"
TypeResolver TR1 = env -> null
EnumValuesProvider EVP1 = name -> null

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these lines, as the default is strictness on

.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF1))

.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1)) // Cannot redefine the same field's datafetcher

then:
def e1 = thrown(StrictModeWiringException)
e1.message == "The type Foo has already been defined"
e1.message == "The field foo on type Foo has already been defined"

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))

Expand All @@ -222,7 +219,6 @@ class RuntimeWiringTest extends Specification {

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
then:
Expand All @@ -231,15 +227,13 @@ class RuntimeWiringTest extends Specification {

when:
RuntimeWiring.newRuntimeWiring()
.strictMode()
.scalar(Scalars.GraphQLString)
then:
def e4 = thrown(StrictModeWiringException)
e4.message == "The scalar String is already defined"

when:
TypeRuntimeWiring.newTypeWiring("Foo")
.strictMode()
.defaultDataFetcher(DF1)
.defaultDataFetcher(DF2)

Expand All @@ -248,34 +242,148 @@ class RuntimeWiringTest extends Specification {
e5.message == "The type Foo has already has a default data fetcher defined"
}

def "overwrite default data fetchers if they are null"() {

def "strict mode, on by default, permits a type to be defined more than once as long as elements are not overlapping"() {
DataFetcher DF1 = env -> "x"
DataFetcher DF2 = env -> "x"
DataFetcher DEFAULT_DF = env -> null
DataFetcher DEFAULT_DF2 = env -> null
TypeResolver TR1 = env -> null
EnumValuesProvider EVP1 = name -> null

when:
def runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF))
// Permit type wiring to be defined more than once, if child DataFetchers are for distinct fields
def runtimeWiring1 = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF2))
.build()

then:
runtimeWiring.getDefaultDataFetcherForType("Foo") == DEFAULT_DF
noExceptionThrown()
runtimeWiring1.getDataFetchers().get("Foo").get("foo") == DF1
runtimeWiring1.getDataFetchers().get("Foo").get("bar") == DF2

when:
// Only one type wiring is allowed per type, do not allow redefinition
RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))

then:
def e2 = thrown(StrictModeWiringException)
e2.message == "The type Foo already has a type resolver defined"

when:
// Only one enum values provider is allowed per type, do not allow redefinition
RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))

then:
def e3 = thrown(StrictModeWiringException)
e3.message == "The type Foo already has a enum provider defined"

when:
// Only one scalar wiring is allowed per scalar
RuntimeWiring.newRuntimeWiring()
.scalar(Scalars.GraphQLString)
then:
def e4 = thrown(StrictModeWiringException)
e4.message == "The scalar String is already defined"

when:
// Only one default data fetcher is allowed, do not allow redefinition
TypeRuntimeWiring.newTypeWiring("Foo")
.defaultDataFetcher(DF1)
.defaultDataFetcher(DF2)

then:
def e5 = thrown(StrictModeWiringException)
e5.message == "The type Foo has already has a default data fetcher defined"
}

def "strict mode, if set to off, won't stop certain redefinitions"() {
Copy link
Member Author

@dondonz dondonz Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests to lock in the strict mode off case

DataFetcher DF1 = env -> "x"
DataFetcher DF2 = env -> "x"
TypeResolver TR1 = env -> null
EnumValuesProvider EVP1 = name -> null

when:
runtimeWiring = RuntimeWiring.newRuntimeWiring()
def runtimeWiring1 = RuntimeWiring.newRuntimeWiring()
.strictMode(false)
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF2))
.build()

then:
noExceptionThrown()
runtimeWiring1.getDataFetchers().get("Foo").get("foo") == DF2

when:
def runtimeWiring2 = RuntimeWiring.newRuntimeWiring()
.strictMode(false)
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").typeResolver(TR1))
.build()

then:
noExceptionThrown()
runtimeWiring2.typeResolvers.get("Foo") == TR1

when:
def runtimeWiring3 = RuntimeWiring.newRuntimeWiring()
.strictMode(false)
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").enumValues(EVP1))
.build()

then:
noExceptionThrown()
runtimeWiring3.getEnumValuesProviders().get("Foo") == EVP1

when:
def runtimeWiring4 = RuntimeWiring.newRuntimeWiring()
.strictMode(false)
.scalar(Scalars.GraphQLString)
.build()

then:
noExceptionThrown()
runtimeWiring4.scalars.get("String") == Scalars.GraphQLString

when:
def typeRuntimeWiring = TypeRuntimeWiring.newTypeWiring("Foo")
.strictMode(false)
.defaultDataFetcher(DF1)
.defaultDataFetcher(DF2)
.build()

then:
noExceptionThrown()
typeRuntimeWiring.defaultDataFetcher == DF2
}

def "when strict mode on, do not allow default data fetcher redefinition"() {
DataFetcher DF1 = env -> "w"
DataFetcher DEFAULT_DF = env -> "x"
DataFetcher DEFAULT_DF2 = env -> "y"

// Having a datafetcher and a default for the type is ok
when:
def runtimeWiring1 = RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("foo", DF1))
.type(TypeRuntimeWiring.newTypeWiring("Foo").dataFetcher("bar", DF2))
// we can specifically overwrite it later
.type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF2))
.build()

then:
runtimeWiring.getDefaultDataFetcherForType("Foo") == DEFAULT_DF2
runtimeWiring1.getDefaultDataFetcherForType("Foo") == DEFAULT_DF

// Do not permit redefinition of the default datafetcher
when:
RuntimeWiring.newRuntimeWiring()
.type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF))
.type(TypeRuntimeWiring.newTypeWiring("Foo").defaultDataFetcher(DEFAULT_DF2))
.build()

then:
def error = thrown(StrictModeWiringException)
error.message == "The type Foo already has a default data fetcher defined"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covering a new edge case, where the default datafetcher is redefined at the runtime wiring level

We already had a test for when it is redefined inside the type wiring

}
}
Loading
Loading