HotChocolate (GraphQL) schema first approach on complex type - graphql

I'm novice in HotChocolate and I'm trying to PoC some simple usage.
I've created very simple .graphql file:
#camera.graphql
type Camera {
id: ID!
name: String!
}
type Query {
getCamera: Camera!
}
And a very simple .NET code for camera wrapping:
public class QlCamera
{
public static QlCamera New()
{
return new QlCamera
{
Id = Guid.NewGuid().ToString(),
Name = Guid.NewGuid().ToString()
};
}
public string Id { get; set; }
public string Name { get; set; }
}
as well as such for schema creation:
public void CreateSchema()
{
string path = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var smBuilder = SchemaBuilder.New();
smBuilder.AddDocumentFromFile(path + "/GraphQL/camera.graphql");
smBuilder.AddResolver("Query", "getCamera", () => QlCamera.New());
var schema = smBuilder.Create();
}
On the last line however I do get an exception :
HotChocolate.SchemaException: 'Multiple schema errors occured:
The field Camera.id has no resolver. - Type: Camera
The field Camera.name has no resolver. - Type: Camera
'
I've tried to create :
public class QlCameraType : ObjectType<QlCamera>
{
protected override void Configure(IObjectTypeDescriptor<QlCamera> descriptor)
{
descriptor.Name("Camera");
descriptor.Field(t => t.Id).Type<NonNullType<StringType>>();
descriptor.Field(t => t.Name).Type<StringType>();
}
}
and to replace
smBuilder.AddResolver("Query", "getCamera", () => QlCamera.New());
with
smBuilder.AddResolver("Query", "getCamera", () => new QlCameraType());
But I continue to get the same exception.
Obviously I miss something here, But I cannot understand what exactly.
Could someone explain me what I do miss ?
(I've passed few times trough the documentation, but I cannot find relevant help there)

As exception clearly states - there are no revolvers bind for the particular fields ("id" and "name") of the "Camera" type/object.
So they just have to be added with :
smBuilder.AddResolver("Camera", "id", rc => rc.Parent<QlCamera>().Id);
smBuilder.AddResolver("Camera", "name", rc => rc.Parent<QlCamera>().Name);
And that is it.

Related

GraphQL combining two Resolvers

I currently have two resolvers, Authors and Books, that return data from two separate API's. In most scenarios I only need to call one or the other, however, in this scenario, I need to attach the books to the author.
It's not clear to me how I should do this.
Option 1 - Simple to do
I call the book API in the Author resolver and combine them here. This means I'd potentially make unnecessary calls to the Book API. It also means if the book API changes, I'd have to make updates to both the author and book resolvers instead of just updating the Book resolver.
Option 2 - Resolver
Is there a way to call the Book resolver from within the Author resolver?
Options 3 - Client
Is there a way to stitch the author and book together from within the client query?
I’m new to graphql and type-graphql so apologies if this is obvious.
Author
const author = {
name: 'James',
bookIds: [1, 2]
};
Book
const book = {
id: 1,
title: 'Book 1'
};
Desired outcome
const author = {
name: 'James',
books: [{
id: 1,
title: 'Book 1'
},
{
id: 2,
title: 'Book 2'
}]
}
Resolvers
#Service()
#Resolver(() => Author)
export class AuthorResolver {
constructor(private readonly authorService: authorService) { }
#Query(() => Author)
async author(
#Arg('authorId', () => ID, { nullable: false }) authorId: string,
#Ctx() { dataSources }: ResolverContext
): Promise<Author | undefined> {
const { authorService } = dataSources;
const author = await this.author.getAuthor(authorService, authorId);
return {
id: author.id,
name: author.name,
bookIds: author.bookIds
};
}
}
#Service()
#Resolver(() => Book)
export class BookResolver {
constructor(private readonly bookService: bookService) { }
#Query(() => Book)
async book(
#Arg('bookId', () => ID, { nullable: false }) bookId: string,
#Ctx() { dataSources }: ResolverContext
): Promise<Book | undefined> {
const { bookService } = dataSources;
const book = await this.book.getBook(bookService, bookId);
return {
id: book.id,
title: book.title
};
}
}
Client Side Query
query BookQuery($bookId: ID!) {
book(bookId: $bookId) {
id
title
}
}
query authorQuery($authorId: ID!) {
book(authorId: $authorId) {
id
name
books
}
}
You must implement a FieldResolver, called books, in the Author resolver.
If an API changes in the future, it will not affect the resolver since you use a service that talks to the API and acts as a middleware layer.
The service must be well implemented (abstraction) and the returned entity must be matched/mapped correctly to a GraphQL object type. i.e. there is no need to return {id: author.id, ...} inside a resolver since it's done automatically by the service and class mappings. 
Moreover, you inject a service instance inside the resolver, so there is no need to use #Ctx and obtain the same service instance: simply use this.[SERVICE_NAME].[METHOD].
Keep you context as simple as possible (e.g. authenticated user id obtained by a JWT).
The final Author resolver is much cleaner and more portable:
#Resolver(() => Author)
#Service()
export class AuthorResolver {
#Inject()
private readonly authorService!: AuthorService;
#Inject()
private readonly bookService!: BookService;
// 'nullable: false' is default behaviour
// No need for '#Ctx' here
// Returned value is inferred from service -> 'Promise<Author | undefined>'
#Query(() => Author)
async author(#Arg('id', () => ID) id: string) {
return this.authorService.findOne(id);
}
#FieldResolver(() => [Book])
async books(#Root() author: Author) {
return this.bookService.findManyByAuthorId(author.id);
// OR 'return this.bookService.findMany(author.bookIds);'
}
}
If you want an example project, see this.

Understanding React-Relay Connections in the Context of Type-GraphQL

The below code excerpt comes from the React-Relay docs on Rendering Connections. I am wondering if someone could provide me with an example of what the underlying schema definition (using `type-graphql for annotations/decorations) would look like.
const {graphql} = require('RelayModern');
const userFragment = graphql`
fragment UserFragment on User {
name
friends(after: $cursor, first: $count)
#connection(key: "UserFragment_friends") {
edges {
node {
...FriendComponent
}
}
}
}
`;
Would it look something like the following? With attention paid to the UserType type definition, and especial attention to the friends field. I am also hoping if anyone could turn my attention to a more elaborated upon example/boilerplate to help me understand what is compliant with the Relay specification. Some examples I am after:
How to type the return type of a Query if I intend one of the Query's resolved fields to be a Connection type? And what would this look when written as a fragment.
How to type the same scenario as above, except now the return type is an iterable of the original return type?
#ObjectType({ implements: [Node] })
export class UserType extends Node {
#Field()
name: string
#Field()
friends: UserConnection
}
const User = createUnionType({
name: 'User',
types: () => [UserType] as const,
resolveType: value => {
// if ('code' in value) {
// return Error
// }
return UserType
}
})
#ObjectType()
export class UserEdge extends EdgeType('report', User) {}
#ObjectType()
export class UserConnection extends ConnectionType<UserEdge>(
'user',
UserEdge
) {
}

How to pass file in my GraphQL query for my Integration test?

So I'm trying to write an Integration test for my query that accepts a file (Upload scalar) from Apollo.
#Test
void imageFromSimilarImage() throws IOException {
when(imageService.findBySimilarImage(/*** some-file ***/)).thenReturn(TEST_IMAGE_LIST);
GraphQLResponse response = graphQLTestTemplate.postForResource("graphql/image-from-similar-image.graphql");
assertThat(response.isOk()).isTrue();
assertThat(response.getList("$.data.imageFromSimilarImage", Image.class)).contains(TEST_IMAGE_A);
}
my image-from-similar-image.graphql file:
query {
imageFromSimilarImage(file: /*** "some-file-content" ***/) {
url
cloudinaryId
tags {
value
}
}
}
My original graphql schema, image.graphql file, if needed...
scalar Upload
schema {
query: Query
mutation: Mutation
}
type Image {
id: ID!
cloudinaryId: String!
url: String!
tags: [Tag]
}
type Tag {
id: ID!
value: String!
image: Image!
}
type Query {
imageFromTag(tags : [String!]!): [Image]
imageFromSimilarImage(file: Upload!): [Image]
allImages : [Image]
}
type Mutation {
createImage(files: [Upload!]!) : [Image]
}
Any ideas? I've browsed around for this , however couldn't find anything, with Junit ...
I solved this by changing the signature of my query imageFromSimilarImage to the following:
imageFromSimilarImage(files: [Upload!]!) : [Image]
Basically what this allowed me to do, is to pass an empty array in my test graphql file,
query {
imageFromSimilarImage(file: []) {
url
cloudinaryId
tags {
value
}
}
}
and finally in my test case:
#Test
void imageFromSimilarImage() throws IOException {
when(imageService.findBySimilarImage(anyList())).thenReturn(TEST_IMAGE_LIST);
GraphQLResponse response = graphQLTestTemplate.postForResource("graphql/image-from-similar-image.graphql");
assertThat(response.isOk()).isTrue();
assertThat(response.getList("$.data.imageFromSimilarImage", Image.class)).contains(TEST_IMAGE_A);
}
However, this does change the behavior of the query, as in it accepts Multiple files, now instead of just one, although in my case this does not affect anything.
Just keep this in mind, and make sure it is acceptable in your case as well.

Abstract object not mapped correctly in Elasticsearch using Nest 7.0.0-alpha1

I am using NEST (.NET 4.8) to import my data, and I have a problem getting the mapping to work in NEST 7.0.0-alpha1.
I have the following class structure:
class LinkActor
{
public Actor Actor { get; set; }
}
abstract class Actor
{
public string Description { get; set; }
}
class Person : Actor
{
public string Name { get; set; }
}
I connect to Elasticsearch this way:
var connectionSettings = new ConnectionSettings(new Uri(connection));
connectionSettings.DefaultIndex(indexName);
var client = new ElasticClient(connectionSettings);
The actual data looks like this:
var personActor = new Person
{
Description = "Description",
Name = "Name"
};
var linkActor = new LinkActor
{
Actor = personActor
};
And the data is indexed like this:
result = client.IndexDocument(linkActor);
Using NEST 6.6 I am getting the following data in Elasticsearch 6.5.2:
"actor": {
"name": "Name",
"description": "Description"
}
However when using NEST 7.0.0-alpha1 I get the following data in Elasticsearch 7.0.0:
"actor": {
"description": "Description"
}
So the data from the concrete class is missing. I am obviously missing / not understanding some new mapping feature, but my attempts with AutoMap has failed:
client.Map<(attempt with each of the above classes)>(m => m.AutoMap());
Is is still possible to map the data from the concrete class in NEST 7.0.0-alpha1?
I found a workaround using the NEST.JsonNetSerializer (remember to install this), which allows me to pass a JObject directly:
Connect to Elasticsearch using a pool so you can add the JsonNetSerializer.Default:
var pool = new SingleNodeConnectionPool(new Uri(connection));
var connectionSettings = new ConnectionSettings(pool, JsonNetSerializer.Default);
connectionSettings.DefaultIndex(indexName);
var client = new ElasticClient(connectionSettings);
Convert the linkActor object from above to a JObject (JsonSerializerSettings omitted for clarity, add them to get CamelCasing):
var linkActorSerialized = JsonConvert.SerializeObject(linkActor);
var linkActorJObject = JObject.Parse(linkActorSerialized);
result = client.IndexDocument(linkActorJObject);
This gives the desired result:
"actor": {
"name": "Name",
"description": "Description"
}
It is a workaround, hopefully someone will be able to explain the mapping in the question.

how to use pascal casing instead of camel casing in graphql dotnet

here I have a mutation, as you can see the parameter name is Product with capital P and the field name is CreateProduct with capital C when I execute this mutation from graphical I have to write the name of the field on camel casing and also the name of the parameter, is there a way to configure graphql-dotnet to respect the names as they are written in the code?
const string STR_Product = "Product";
public Mutations(IProductService ProductService)
{
Name = "Mutation";
Field<ProductType>(
"CreateProduct",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ProductInputType>> { Name = STR_Product }),
resolve: context =>
{
var ProductInput = context.GetArgument<ProductInput>(STR_Product);
return ProductService.CreateAsync(ProductInput.Code, ProductInput.Name, ProductInput.Description);
//return new ProductInputType();
}
);
}
}
You can pass a IFieldNameConverter to the ExecutionOptions. It defaults to using CamelCaseFieldNameConverter.
Some special considerations are needed for the Introspection types, as those are required to be camel case according to the GraphQL Specification.
GraphQL.NET provides CamelCaseFieldNameConverter, PascalCaseFieldNameConverter, and DefaultFieldNameConverter. You could also write your own. Source found here.
using System;
using GraphQL;
using GraphQL.Types;
public class Program
{
public static void Main(string[] args)
{
var schema = Schema.For(#"
type Query {
Hello: String
}
");
var json = schema.Execute(_ =>
{
_.Query = "{ Hello }";
_.Root = new { Hello = "Hello World!" };
_.FieldNameConverter = new DefaultFieldNameConverter();
});
Console.WriteLine(json);
}
}
See the above answer by Joe McBride, but instead of "FieldNameConverter" should be using "NameConverter". Example:
var json = schema.Execute(_ =>
{
_.NameConverter = new DefaultNameConverter(); //or PascalNameConverter, etc.
});
(posted as an answer because I am unable to comment)

Resources