-
Notifications
You must be signed in to change notification settings - Fork 317
Question: How to generate the schema and wiring at runtime? #452
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
Comments
Perhaps this could be built as a feature to create schema dynamically at startup, based Controllers and entities? If yes, please don't forget the documentation of schema generation too. Also, not all entity properties need to be exposed to schema. |
In my case I need to be able to generate the fields that are present in a specific structure, and the schema which must then be linked to a specific getter call at run time for each field. To over simplify what I'm looking for:
If you know all fields at the time you write the code this is trivially hardcoded. As you can see my latest experiment does generate the Schema itself, yet I have not been able to link these fields to code that provides the required value to the client. |
Apologies for the slow response. The schemaFactory hook you're using takes two inputs, the By default, we use |
I'm still learning how all of these parts work together. |
After you mentioning me not doing anything with the RuntimeWiring I did some more experimenting. I now have a thing that @Configuration(proxyBeanMethods = false)
public class MyDynamicGraphQLApi {
private static final Logger LOG = LogManager.getLogger(MyDynamicGraphQLApi.class);
@Bean
GraphQlSourceBuilderCustomizer graphQlSourceBuilderCustomizer() {
return builder -> {
// New type
GraphQLObjectType version = GraphQLObjectType
.newObject()
.name("Version")
.description("The version info")
.field(newFieldDefinition()
.name("commit")
.description("The commit hash")
.type(Scalars.GraphQLString)
.build())
.field(newFieldDefinition()
.name("url")
.description("Where can we find it")
.type(Scalars.GraphQLString)
.build())
.build();
// New "function" to be put in Query
GraphQLFieldDefinition getVersion = newFieldDefinition()
.name("getVersion")
.description("It should return the do something here")
.argument(GraphQLArgument.newArgument().name("thing").type(Scalars.GraphQLString).build())
.type(version)
.build();
// Wiring for the new type
TypeRuntimeWiring typeRuntimeWiringVersion =
TypeRuntimeWiring
.newTypeWiring("Version")
.dataFetcher("commit", testDataFetcher)
.dataFetcher("url", testDataFetcher)
.build();
// Wiring for the new "function"
TypeRuntimeWiring typeRuntimeWiringQuery =
TypeRuntimeWiring
.newTypeWiring("Query")
.dataFetcher("getVersion", getVersionDataFetcher)
.build();
builder
.schemaFactory(
(typeDefinitionRegistry, runtimeWiring) -> {
// NOTE: Spring-Graphql DEMANDS a schema.graphqls with a valid schema or it will not load...
// ---------------------------------
// Extending the entries in Query
// We get the existing Query from the typeDefinitionRegistry (defined in the schema.graphqls file).
ObjectTypeDefinition query = (ObjectTypeDefinition) typeDefinitionRegistry.getType("Query").orElseThrow();
NodeChildrenContainer namedChildren = query.getNamedChildren();
List<Node> fieldDefinitions = namedChildren.getChildren(CHILD_FIELD_DEFINITIONS);
// We add all our new "functions" (field Definitions) that need to be added to the Query
fieldDefinitions.add(convert(getVersion));
// Add them all as extra fields to the existing Query
ObjectTypeDefinition queryWithNewChildren = query.withNewChildren(namedChildren);
// We remove the old "Query" and replace it with the version that has more children.
typeDefinitionRegistry.remove(query);
typeDefinitionRegistry.add(queryWithNewChildren);
// -----------------------
// Add all additional types (outside of Query)
typeDefinitionRegistry.add(convert(version));
// -----------------------
// Add all additional wiring
// NASTY 3: There is no simple 'addType' on an existing instance of RuntimeWiring.
addType(runtimeWiring, typeRuntimeWiringQuery);
addType(runtimeWiring, typeRuntimeWiringVersion);
// Now we create the Schema.
return new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
}
);
};
}
static DataFetcher<?> getVersionDataFetcher = environment -> {
String arguments = environment
.getArguments()
.entrySet()
.stream()
.map(entry -> "{ " + entry.getKey() + " = " + entry.getValue().toString() + " }")
.collect(Collectors.joining(" | "));
String result = "getVersion Fetch: %s(%s)".formatted(environment.getField(), arguments);
LOG.info("{}", result);
return result;
};
static DataFetcher<?> testDataFetcher = environment -> {
String arguments = environment
.getArguments()
.entrySet()
.stream()
.map(entry -> "{ " + entry.getKey() + " = " + entry.getValue().toString() + " }")
.collect(Collectors.joining(" | "));
String result = "Fetch: %s(%s)".formatted(environment.getField().getName(), arguments);
LOG.info("{}", result);
return result;
};
private FieldDefinition convert(GraphQLFieldDefinition field) {
// NASTY: So far the only way I have been able to find to this conversion is to
// - wrap it in a GraphQLObjectType
// - Print it
// - and parse that String
GraphQLObjectType query = GraphQLObjectType
.newObject()
.name("DUMMY")
.field(field)
.build();
String print = new SchemaPrinter().print(query);
ObjectTypeDefinition dummy = (ObjectTypeDefinition)new SchemaParser().parse(print).getType("DUMMY").orElseThrow();
return dummy.getFieldDefinitions().get(0);
}
private TypeDefinition convert(GraphQLObjectType objectType) {
String print = new SchemaPrinter().print(objectType);
return new SchemaParser().parse(print).getType(objectType.getName()).orElseThrow();
}
// Yes, I know NASTY HACK
public void addType(RuntimeWiring runtimeWiring, TypeRuntimeWiring typeRuntimeWiring) {
Map<String, Map<String, DataFetcher>> dataFetchers = runtimeWiring.getDataFetchers();
String typeName = typeRuntimeWiring.getTypeName();
Map<String, DataFetcher> typeDataFetchers = dataFetchers.computeIfAbsent(typeName, k -> new LinkedHashMap<>());
typeDataFetchers.putAll(typeRuntimeWiring.getFieldDataFetchers());
TypeResolver typeResolver = typeRuntimeWiring.getTypeResolver();
if (typeResolver != null) {
runtimeWiring.getTypeResolvers().put(typeName, typeResolver);
}
}
} Although this I'm looking forward to learn on how to get to this end result in a clean way. |
Hi there. I am a maintainer on the graphql-java engine project. @rstoyanchev asked me to chime in here on possible approaches. Looking at your example you dont seem to far off however you are using the the AST classes instead of the GraphQlXXX schema element classes. SDL text such as Rather than do this you should build the base @Bean
GraphQlSourceBuilderCustomizer graphQlSourceBuilderCustomizer() {
return builder -> {
builder
.schemaFactory(
(typeDefinitionRegistry, runtimeWiring) -> {
// Now we create the base Schema.
GraphQlSchema baseSchema = new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);
GraphQlSchema finalSchema = editBaseSchema(baseSchema);
} The above builds a base schema based on SDL and runtime wiring and then this gets edited via another mechanism. That other mechanism is There is a more documentation on editing a schema here The basic gist is that for runtime schema elements (as opposed to AST element you are working on in your linked code) you use the runtime
The The following is an example of editing (adding) new fields and types. GraphQLTypeVisitorStub visitor = new GraphQLTypeVisitorStub() {
@Override
public TraversalControl visitGraphQLObjectType(GraphQLObjectType objectType, TraverserContext<GraphQLSchemaElement> context) {
GraphQLCodeRegistry.Builder codeRegistry = context.getVarFromParents(GraphQLCodeRegistry.Builder.class);
// we need to change __XXX introspection types to have directive extensions
if (someConditionalLogic(objectType)) {
GraphQLObjectType newObjectType = buildChangedObjectType(objectType, codeRegistry);
return changeNode(context, newObjectType);
}
return CONTINUE;
}
private boolean someConditionalLogic(GraphQLObjectType objectType) {
// up to you to decide what causes a change, perhaps a directive is on the element
return objectType.hasDirective("specialDirective");
}
private GraphQLObjectType buildChangedObjectType(GraphQLObjectType objectType, GraphQLCodeRegistry.Builder codeRegistry) {
GraphQLFieldDefinition newField = GraphQLFieldDefinition.newFieldDefinition()
.name("newField").type(Scalars.GraphQLString).build();
GraphQLObjectType newObjectType = objectType.transform(builder -> builder.field(newField));
DataFetcher newDataFetcher = dataFetchingEnvironment -> {
return "someValueForTheNewField";
};
FieldCoordinates coordinates = FieldCoordinates.coordinates(objectType.getName(), newField.getName());
codeRegistry.dataFetcher(coordinates, newDataFetcher);
return newObjectType;
}
};
GraphQLSchema newSchema = SchemaTransformer.transformSchema(baseSchema, visitor); (please forgive the code above - I don't have a compiler handy so its illustrative and not necessarily compilable.) |
Thanks this is really helpful. I'm going to try to get this running at my end. One thing I find confusing:
I would expect a DAG, is this a typo in the website? |
@bbakerman @rstoyanchev Thanks for helping out! With your help I was able to implement what I was looking for: Looking now at the Spring-GraphQL API and this solution direction, I'm now thinking that perhaps the Spring-GraphQL API for customization should be different. Looking at what I know now perhaps a better API is simply a way of providing an instance of a And perhaps even allow for a system to have multiple of those? That way libraries can be created that you simply include as a dependency and they add packaged features to the GraphQL api of a system, thus allowing for a kind of So thanks for helping out and since my question has been answered I'm closing this issue. |
@nielsbasjes thanks for the feedback. I see what you've arrived at with the schemaFactory that applies visitors with We actually do support registration of |
Hi,
For a project where the schema is to be determined at the start of the application (it can change depending on configuration) I'm looking at generating the schema and all related things at startup.
I have put my efforts (as clean as possible) in this test project.
https://github.com/nielsbasjes/spring-gql-test/blob/main/src/main/java/nl/basjes/experiments/springgqltest/MyDynamicGraphQLApi.java
Works partially.
The schema that is generated is what I have in mind for this test.
Yet when running this query:
I expect this
but I get this:
So as far as I can tell I'm doing something wrong with regards of the runtime wiring.
As I have trouble finding a working demo application that does this I'm asking here.
What am I doing wrong?
What have I overlooked?
Where can I find the documentation I misunderstood?
Thanks.
The text was updated successfully, but these errors were encountered: