How to map nested object query in Spring graphql - spring

Using org.springframework.boot:spring-boot-starter-graphql and WebMvc, I would like to handle, in the #Controller, nested fields.
Schema:
type Movie {
name: String
actor: Actor
}
type Actor {
name: String
homeAddress: Address
officeAddress: Address
}
type Address {
street: String
city: String
}
query:
query {
movie(name: "xyz") {
name
actor {
name
homeAddress {
street
city
}
}
}
}
The #Controller handles the first field with:
#QueryMapping
public Movie movie(#Argument name) {...}
2nd with schemaMapping:
#SchemaMapping(type=“Movie”, field=“actor”)
public Actor actor(Movie movie) {...}
But how to handle the third level field (address)?
#SchemaMapping(typeName=“Actor”, field=“homeAddress”)
public Address homeAddress(Actor actor) {...}
this doesn't work

Related

HotChocolate GraphQL query to page/filter/sort on nested array fields

HotChocolate Version=12.3.2.0
I want to be able to page/filter/sort on nested fields. For example, where user id = 1234, get the user's 1st document set, then the 1st docFile in the document set, ordered by docFile createdDate.
public class User
{
public int Id {get;set}
[UsePaging]
[UseFiltering]
[UseSorting]
public List<Document> Documents { get; set; }
}
public class Document
{
[UsePaging]
[UseFiltering]
[UseSorting]
public List<DocFile> DocFiles { get; set; }
public User User {get;set;}
}
public class DocFile
{
public string Name {get;set}
public DateTime CreatedDate {get;set;}
public Document Document {get;set;}
}
[UseAppDbContext]
[UsePaging]
[UseProjection]
[UseFiltering]
[UseSorting]
public async Task<Connection<User>> GetUsersAsync(
IResolverContext context,
[ScopedService] DbContext dbContext,
CancellationToken cancellationToken
)
{
var dbResult = dbContext.Users.Filter(context).Sort(context).Project(context).ToArray();
var result = await dbResult.ApplyCursorPaginationAsync(context, cancellationToken);
return result;
}
GraphQL Query
users(
where: {id: {eq: 1234}}
) {
nodes {
documents(first:1){
id
files(first:1 order:{createdDate: DESC}) {
nodes {
name
createdDate
}
}
}
}
}
But when I execute the GraphQL query I currently get the following error:
"exceptionType": "InvalidOperationException",
"message": "No generic method 'OrderByDescending' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. "
Any idea on how to do this?
If the [UseSorting] annotation comes from HotChocolate.Types it's the old way of filtering (uses a different syntax). Then it should be order_by in your query.
Try [HotChocolate.Data.UseSorting] to match your query.

HotChocolate GraphQL Configure not being called

I have a Query:
public class Query : ObjectType
{
protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
{
Console.WriteLine("Hit Configure");
}
public IQueryable<DataStory> GetDataStories([Service]MicipContext context)
{
return context.DataStories;
}
}
And in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddGraphQL(sp =>
{
var schema = SchemaBuilder.New()
.AddDocumentFromString(this.ReadSchema())
.BindResolver<Query>(c => c.To<Query>())
.AddServices(sp)
.Create();
return schema;
}
}
And my schema graphql:
type Query {
dataStories: [DataStory!]!
}
type DataStory {
id: Int!
title: String!
}
When I call the query with:
query GetDataStories {
dataStories {
title
}
}
The resolver returns correctly but my configure method is never called. What am I doing wrong? Shouldn't Configure be called at some point?
Figured out that Hot Chocolate has not added support for pagination/sort/filter on schema first projects. We are doing schema first so we have to implement it ourselves.

How to handle List<Map<String, Object>> return type from query resolver in .graphqls

I have a graphql implementation, below is my .graphqls file
schema {
query: Query
}
type Product {
id: Int!
name: String!
description: String!
productTypes: [ProductType]!
}
type City {
id: Int!
name: String!
}
type ProductType {
id: Int!
type: String!
product: Int
}
type Query {
products: [Product]!
cities: [City]!
productTypes: [ProductType]!
}
This is my query resolver code
public class Query implements GraphQLQueryResolver {
#Autowired
ProductRespository productRespository;
#Autowired
EntityManager em;
#Autowired
CityRepository cityRepository;
#Autowired
ProductTypeRepository productTypeRepository;
public List<Map<String, Object>> products() {
javax.persistence.Query q = em.createQuery("select new
map(p.name as productName, p.id as productId) from Product p");
List<Map<String, Object>> list = q.getResultList();
return list;
}
public Iterable<City> cities() {
return cityRepository.findAll();
}
}
when I am running the application I am getting below error
Type java.util.Map cannot be
mapped to a GraphQL type! Since GraphQL-Java deals with erased types at
runtime, only non-parameterized classes can represent a GraphQL type.
This allows for reverse-lookup by java class in interfaces and union
types
So how I can handle this kind of return type in Query Section of .graphqls
Thanks in Advance

How to use another variable name or How to flatten entity in JPA Projection for nested object

I'm making an api for querying nested entity with Spring Data JPA Projection.
My Code
The Entity:
#Entity
class User {
#Id
var userId: String
var name: String
var age: Int
#OneToOne
#JoinColumn(name = "userId")
var address: Address
}
#Entity
class Address {
var userId: String
var street: String
var city: String
var country: String
}
The Repository:
interface UserView {
val name: String
val address: AddressView
interface AddressView {
val city: String
val country: String
}
}
#Repository
interface UserRepository : JPARepository<User, String> {
fun findAll(): List<UserView>
}
Expected Response
{
"name": "example",
"city": "example-city",
"country": "example-country"
}
My code produces
{
"name": "example",
"address": {
"city": "example-city",
"country": "example-country"
}
}
I Tried
I tried another view to flatten object:
interface UserView {
val name: String
val addressCity: String
val addressCountry: String
}
But this case, the variable naming is too complicate.
I want to solve this problem with projection. How can I solve this problem?
In JPA, you can do this using #NamedNativeQuery only:
#NamedNativeQuery(
name = "getUser",
query = "SELECT u.name, a.city, a.country FROM User u and Address a where a.userId = u.id ", resultClass=UserView .class)
#Entity
class User {
...
}
For Reference hibernate-named-query
Try this:
data class UserView(name: String, city: String, country: String)
#Repository
interface UserRepository : JPARepository<User, String> {
#Query(value = "select new your.pkg.UserView(u.name, u.address.city, u.address.country) from User u")
fun findAllFlat(): List<UserView>
}
You can use #Value and combine more fields or even access the fields of objects.
From the spring-data-rest documentation:
You can create a projection that combines the two data fields in the preceding example together, as follows:
#Projection(name = "virtual", types = { Person.class })
public interface VirtualProjection {
#Value("#{target.firstName} #{target.lastName}")
String getFullName();
}
Spring’s #Value annotation lets you plug in a SpEL expression that takes the target object and splices together its firstName and lastName attributes to render a read-only fullName.
Flattening also works for me:
#Value("#{target.document.title}")
String getDocumentTitle();

Filtering schema out of NSwag swagger

I have an ASP.NET full framework application with API endpoints. I am using NSwag to generate a swagger document. This all works.
I need to generate a document for only a small subset of the endpoints. The paths are filtered, but the schema is not. How do I filter the schema objects to match the path filtering?
Example:
I have this filter
public class IncludeControllersInSwagger : IOperationProcessor
{
public Task<bool> ProcessAsync(OperationProcessorContext context)
{
return Task.FromResult(
context.ControllerType == typeof(ControllerA));
}
}
And this at startup:
settings.GeneratorSettings.OperationProcessors.Add(new IncludeControllersInSwagger());
The controllers are:
public class AResponse
{
public string Message { get; set; }
public bool Flag { get; set; }
}
public class BResponse
{
public string Message { get; set; }
public int Count { get; set; }
}
[Route("a")]
public class ControllerA : ApiController
{
[HttpGet]
public AResponse Get()
{
return new AResponse
{
Message = "Hello from A",
Flag = true
};
}
}
[Route("b")]
public class ControllerB : ApiController
{
[HttpGet]
public BResponse Get()
{
return new BResponse
{
Message = "Hello from B",
Count = 42
};
}
}
Then the generated swagger contains just one path:
"paths": {
"/a": {
"get": { .. etc
}
}
And that's all, this is correct.
But the schemas contains both:
"schemas": {
"AResponse": {
"type": "object",
"additionalProperties": false,
etc
},
"BResponse": {
"type": "object",
"additionalProperties": false,
etc
}
}
}
The BResponse type should not be there. How do you remove it?
This extra data makes the Schemas section extremely verbose and unsuitable for public documentation in the case where there are over 10 endpoints, and only 2 are exposed via a gateway and therefor documented in swagger.
There is a ISchemaProcessor but it does not return a Boolean like the IOperationProcessor.
Have you tried to add the operation filter as first element?
i.e. OperationProcessors.Insert(0, new IncludeControllersInSwagger())
I think this is important as it will filter out the operation before the dto schemas are generated and added to the document.
This is not an answer to your problem, as you already got an answer that seems to work. I do have a suggestion however. Instead of checking the type of controller in your processor, I would suggest to create a custom attribute:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class IncludeInSwaggerAttribute : Attribute
{
}
Then change your processor to look for this attribute:
public class IncludeInSwaggerOperationProcessor : IOperationProcessor
{
public async Task<bool> ProcessAsync(OperationProcessorContext context)
{
return context.ControllerType.GetCustomAttributes<IncludeInSwaggerAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes<IncludeInSwaggerAttribute>().Any();
}
}
This way, you can add the attribute to any new controller you want to include in swagger without having to change your processor. You can also add the attribute on a single action to only include that action, leaving the rest of the actions in the controller out of swagger.

Resources