I have an entity (Customer) that needs to pull data from multiple sources. The schema looks roughly like this:
{
id: string
name: string
address: string
contact: string
status: string
}
The id, name and address come from an EF datacontext. The contact and status fields come from a single REST endpoint, and looks like this:
GET /url/customer?id=1234
{
id: '1234'
contact: 'joe#bloggington.com'
status: 'ACTIVE'
}
If I put both contact and status into a single field/object (i.e. ContactStatus), then it would be a simple case of creating an extension for Customer. But these fields are not related, and should be regarded as different top-level fields.
Is there a way to ensure that the REST endpoint is called only once, when fetching all values? Essentially resolving both fields when fetching one or the other maybe?
Hot Chocolate v12.15.0, net6.0
Yes you can use the batching api to do this
Create a DataLoader that loads the data from the rest endpoint. This way you can also optimize the fetches from the rest endpoint (if the endpoint supports somthing like /url/customer?ids=1234,2345,5930)
e.g. class YourDataloader extends BatchDataLoader<int, AdditionalCustomerData>
Then you can just do
[ExtendObjectType<Customer>]
public class CustomerExtensions
{
public Task<string> GetContactAsync(
[Parent]Customer customer,
YourDataloader dataloader)
{
var result = await dataloader.LoadAsync(customer.Id);
return result.Contact;
}
public Task<Status> GetStatusAsync(
[Parent]Customer customer,
YourDataloader dataloader)
{
var result = await dataloader.LoadAsync(customer.Id);
return result.Status;
}
}
Related
I am new to HotChocolate and GraphQL as a whole and am trying to grasp on enabling Nodes and/or Relay support for my GraphQL API. Currently using HotChocolate v12.
Context
I am trying to create a mutation that updates an entity (Client in this example). I am using code-first approach here and have the following:
An input record/class is defined as follows:
public record UpdateClientInput([ID(nameof(Client))] int Id, string Code, string Name, string Subdomain);
The mutation function which returns the payload class:
[UseAppDbContext]
public async Task<UpdateClientPayload> UpdateClientAsync(UpdateClientInput input, [ScopedService] AppDbContext context)
{
var client = await context.Set<Client>().FirstOrDefaultAsync(x => x.Id == input.Id);
// cut for brevity
return new UpdateClientPayload(client);
}
I have enabled support for Nodes by adding this in the services configuration:
builder.Services
.AddGraphQLServer()
.AddQueryType(d => d.Name("Query"))
.AddMutationType(d => d.Name("Mutation"))
// removed others for brevity
.AddGlobalObjectIdentification()
.AddQueryFieldToMutationPayloads();
But yet when I browse the API using Banana Cake Pop, the UpdateClientInput object in the schema definition still uses int instead of the ID! (see screenshot below). So the GraphQL client I am using (Strawberry Shake) generates the input object that does not use the ID! type. Am I missing something here?
So to solve this problem, this was actually a change in HotChocolate v12 where records should use the [property: ID] instead of the [ID] attribute. That change is found here https://chillicream.com/docs/hotchocolate/api-reference/migrate-from-11-to-12#records.
What I did is to change the record declaration from:
public record UpdateClientInput([ID(nameof(Client))] int Id, string Code, string Name, string Subdomain);
to
public record UpdateClientInput([property: ID] int Id, string Code, string Name, string Subdomain);
that generated this input object as:
Following the Hot Chocolate workshop and after the 4th step, when running the query
query GetSpecificSpeakerById {
a: speakerById(id: 1) {
name
}
b: speakerById(id: 1) {
name
}
}
I'm getting the following error.
The ID `1` has an invalid format.
Also, the same error is thrown for all queries which have ID as a parameter, maybe this could be a hint, what to check, for me, a person, who just run the workshop it's still unclear.
Based on (not accepted) answer in similar question Error "The ID `1` has an invalid format" when querying HotChocolate, I've checked Relay and it's configuration and looks good.
DI
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(CreateAutomapper());
services.AddPooledDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlite(CONNECTION_STRING).UseLoggerFactory(ApplicationDbContext.DbContextLoggerFactory));
services
.AddGraphQLServer()
.AddQueryType(d => d.Name(Consts.QUERY))
.AddTypeExtension<SpeakerQueries>()
.AddTypeExtension<SessionQueries>()
.AddTypeExtension<TrackQueries>()
.AddMutationType(d => d.Name(Consts.MUTATION))
.AddTypeExtension<SpeakerMutations>()
.AddTypeExtension<SessionMutations>()
.AddTypeExtension<TrackMutations>()
.AddType<AttendeeType>()
.AddType<SessionType>()
.AddType<SpeakerType>()
.AddType<TrackType>()
.EnableRelaySupport()
.AddDataLoader<SpeakerByIdDataLoader>()
.AddDataLoader<SessionByIdDataLoader>();
}
Speaker type
public class SpeakerType : ObjectType<Speaker>
{
protected override void Configure(IObjectTypeDescriptor<Speaker> descriptor)
{
descriptor
.ImplementsNode()
.IdField(p => p.Id)
.ResolveNode(WithDataLoader);
}
// implementation
}
And query itself
[ExtendObjectType(Name = Consts.QUERY)]
public class SpeakerQueries
{
public Task<Speaker> GetSpeakerByIdAsync(
[ID(nameof(Speaker))] int id,
SpeakerByIdDataLoader dataLoader,
CancellationToken cancellationToken) => dataLoader.LoadAsync(id, cancellationToken);
}
But without a bit of luck. Is there something else, what could I check? The full project is available on my GitHub.
I see you enabled relay support on this project.
The endpoint execpts a valid relay ID.
Relay exposes opaque IDs to the client. You can read more about it here:
https://graphql.org/learn/global-object-identification/
In short, a Relay ID is a base64 encoded combination of the typename and the id.
To encode or decode in the browser you can simply use atob and btoa on the console.
So the id "U3BlYWtlcgppMQ==" contains the value
"Speaker
i1"
you can decode this value in the browser with btoa("U3BlYWtlcgppMQ==") and encode the string with
atob("Speaker
i1")
So this query will work:
query GetSpecificSpeakerById {
a: speakerById(id: "U3BlYWtlcgppMQ==") {
id
name
}
}
This issue is migrated from a question on our Github account because we want the answer to be available to others. Here is the original question:
Hello,
Following is the InstanceQuery I tried
http://localhost:3000/3_0_1/Questionnaire/jamana/$graphql?query={id}
I am receiving back response as Cannot query field \"id\" on type \"Questionnaire_Query\"
So what is the right format I should try ?
https://build.fhir.org/graphql.html has a sample as http://test.fhir.org/r3/Patient/example/$graphql?query={name{text,given,family}}.Its working in their server. I cannot get the response When I try similarly in our graphql-fhir.
Original answer from Github:
We are using named queries since we are using express-graphql. I do not believe that is valid syntax. Also, the url provided does not seem to work, I just get an OperationOutcome saying the patient does not exist, which is not a valid GraphQL response.
Can you try changing your query from:
http://localhost:3000/3_0_1/Questionnaire/jamana/$graphql?query={id}
to this:
http://localhost:3000/3_0_1/Questionnaire/jamana/$graphql?query={Questionnaire{id}}
When writing the query, you need to provide the return type as part of the instance query. You should get a response that looks like similar to this(if you have implemented your resolver you will have data and not null):
{
"data": {
"Questionnaire": {
"id": null
}
}
}
and from a later comment:
If you are getting null then you are doing it correctly, but you haven't wrote a query or connected it to a data source. You still need to return the questionnaire in the resolver.
Where you are seeing this:
instance: {
name: 'Questionnaire',
path: '/3_0_1/Questionnaire/:id',
query: QuestionnaireInstanceQuery,
},
You are seeing the endpoint being registered with an id parameter, which is different from a GraphQL argument. This is just an express argument. If you navigate to the questionnaire/query.js file, you can see that the QuestionnaireInstanceQuery query has a different resolver than the standard QuestionnaireQuery. So in your questionnaire/resolver.js file, if you want both query and instance query to work, you need to implement both resolvers.
e.g.
// This is for the standard query
module.exports.getQuestionnaire = function getQuestionnaire(
root,
args,
context = {},
info,
) {
let { server, version, req, res } = context;
// Do query and return questionnaire
return {};
};
// This one is for a questionnaire instance
module.exports.getQuestionnaireInstance = function getQuestionnaireInstance(
root,
args,
context = {},
info,
) {
let { server, version, req, res } = context;
// req.params.id is your questionnaire id, use that for your query here
// queryQuestionnaireById does not exist, it is pseudo code
// you need to query your database here with the id
let questionnaire = queryQuestionnaireById(req.params.id);
// return the correct questionnaire here, default returns {},
// which is why you see null, because no data is returned
return questionnaire;
};
Preface
I want to create a sub-resource of another resource in one call. These resources have a #ManyToMany relationship: Users and Groups.
I do not want to create first a user, then the group and after that the relation as it is shown in Working with Relationships in Spring Data REST - simply because I think a resource that cannot exist on its own, such as a group, should only be created if at least one user is also associated with that resource. For this I require a single endpoint like this one (which is not working for me, otherwise I wouldn't be here) that creates a group and also sets the associated "seeding" user in one transaction.
Currently, the only way to make this work for me is to "synchronize" the relation manually like this:
public void setUsers(Set<AppUser> users) {
users.forEach(u -> u.getGroups().add(this));
this.users = users;
}
this would allow me to
POST http://localhost:8080/groups
{
"name": "Group X",
"users": ["http://localhost:8080/users/1"]
}
but my problem with that is that this does not feel right to me - it does seem like a workaround and not the actual Spring-way to make this requirement work. So ..
I'm currently struggling with creating relational resources using Spring's #RepositoryRestResource. I want to create a new group and associate it with the calling user like this:
POST http://localhost:8080/users/1/groups
{
"name": "Group X"
}
but the only result is the response 204 No Content. I have no idea why. This may or may not be related to another question of mine (see here) where I try to achieve the same by setting the relating resource in the JSON payload - that doesn't work either.
Server side I am getting the following error:
tion$ResourceSupportHttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class org.springframework.hateoas.Resources<java.lang.Object>]]: java.lang.NullPointerException
Please let me know in case you need any specific code.
Tried
I added exported = false to the #RepositoryRestResource of UserGroupRepository:
#RepositoryRestResource(collectionResourceRel = "groups", path = "groups", exported = false)
public interface UserGroupRepository extends JpaRepository<UserGroup, Long> {
List<UserGroup> findByName(#Param("name") String name);
}
and I am sending:
PATCH http://localhost:8080/users/1
{
"groups": [
{
"name": "Group X"
}
]
}
However, the result is still just 204 No Content and a ResourceNotFoundException on the server side.
Unit Test
Essentially, the following unit test is supposed to work but I can also live with an answer why this cannot work and which also shows how this is done correctly.
#Autowired
private TestRestTemplate template;
private static String USERS_ENDPOINT = "http://localhost:8080/users/";
private static String GROUPS_ENDPOINT = "http://localhost:8080/groups/";
// ..
#Test
#DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD)
public void whenCreateUserGroup() {
// Creates a user
whenCreateAppUser();
ResponseEntity<AppUser> appUserResponse = template.getForEntity(USERS_ENDPOINT + "1/", AppUser.class);
AppUser appUser = appUserResponse.getBody();
UserGroup userGroup = new UserGroup();
userGroup.setName("Test Group");
userGroup.setUsers(Collections.singleton(appUser));
template.postForEntity(GROUPS_ENDPOINT, userGroup, UserGroup.class);
ResponseEntity<UserGroup> userGroupResponse = template.getForEntity(GROUPS_ENDPOINT + "2/", UserGroup.class);
Predicate<String> username = other -> appUser.getUsername().equals(other);
assertNotNull("Response must not be null.", userGroupResponse.getBody());
assertTrue("User was not associated with the group he created.",
userGroupResponse.getBody().getUsers().stream()
.map(AppUser::getUsername).anyMatch(username));
}
However, the line
userGroup.setUsers(Collections.singleton(appUser));
will break this test and return a 404 Bad Request.
According to SDR reference:
POST
Only supported for collection associations. Adds a new element to the collection. Supported media types:
text/uri-list - URIs pointing to the resource to add to the association.
So to add group to user try to do this:
POST http://localhost:8080/users/1/groups (with Content-Type:text/uri-list)
http://localhost:8080/groups/1
Additional info.
I know how to retrieve a bean from a service in a datafetcher:
public class MyDataFetcher implements DataFetcher {
...
#Override
public Object get(DataFetchingEnvironment environment) {
return myService.getData();
}
}
But schemas with nested lists should use a BatchedExecutionStrategy and create batched DataFetchers with get() methods annotated #Batched (see graphql-java doc).
But where do I put my getData() call then?
///// Where to put this code?
List list = myService.getData();
/////
public class MyDataFetcher implements DataFetcher {
#Batched
public Object get(DataFetchingEnvironment environment) {
return list.get(environment.getIndex()); // where to get the index?
}
}
WARNING: The original BatchedExecutionStrategy has been deprecated and will get removed. The current preferred solution is the Data Loader library. Also, the entire execution engine is getting replaced in the future, and the new one will again support batching "natively". You can already use the new engine and the new BatchedExecutionStrategy (both in nextgen packages) but they have limited support for instrumentations. The answer below applies equally to both the legacy and the nextgen execution engine.
Look at it like this. Normal DataFetcherss receive a single object as source (DataFetchingEnvironment#getSource) and return a single object as a result. For example, if you had a query like:
{
user (name: "John") {
company {
revenue
}
}
Your company resolver (fetcher) would get a User object as source, and would be expected to somehow return a Company based on that e.g.
User owner = (User) environment.getSource();
Company company = companyService.findByOwner(owner);
return company;
Now, in the exact same scenario, if your DataFetcher was batched, and you used BatchedExecutionStrategy, instead of receiving a User and returning a Company, you'd receive a List<User> and would return a List<Company> instead.
E.g.
List<User> owners = (List<User>) environment.getSource();
List<Company> companies = companyService.findByOwners(owners);
return companies;
Notice that this means your underlying logic must have a way to fetch multiple things at once, otherwise it wouldn't be batched. So your myService.getData call would need to change, unless it can already fetch data for multiple source object in one go.
Also notice that batched resolution makes sense in nested queries only, as the top level resolver can already fetch a list of object, without the need for batching.