GraphQL SPQR - How to get the list of the fields were requested by client using a query - graphql-spqr

is there a way to retrieve the list of the fields were requested in a GraphQL query by a client?
Lets assume I have the following types:
type Book {
isbn: String
title: String
genre: String
author: Author
}
type Author {
name: String
surname: String
age: Int
}
Is there a way on Java side inside a method annotated with #GraphQLQuery to know the fields were requested by the client?
For example, having the following queries:
query {
book ( isbn = "12345" ) {
title
genre
}
}
query {
book ( isbn = "12345" ) {
title
author {
name
surname
}
}
}
I can know the first query requested the fields title and genre of Book
and the second required title from Book and also name and surname of the Author ?
Thanks
Massimo

You can obtain this info via #GraphQLEnvironment. If you just need the immediate subfields, you can inject the names like this:
#GraphQLQuery
public Book book(String isbn, #GraphQLEnvironment Set<String> subfields) {
...
}
If you want all the subfields, or other selection info, you can inject ResolutionEnvironment and get DataFetcherEnvironment from there. That in turn gives you access to the whole SelectionSet.
#GraphQLQuery
public Book book(String isbn, #GraphQLEnvironment ResolutionEnvironment env) {
DataFetchingFieldSelectionSet selection = env.dataFetchingEnvironment.getSelectionSet();
...
}

Related

Prisma schema - Build up a custom object from a relation

Can I build a custom object using a relation, and the properties that it references?
For example, if I have two models like this, where User is populated during authentication:
model User {
id String #id #default(cuid()) #map("_id")
name String?
email String? #unique
}
model Post {
id String #id #default(auto()) #map("_id") #db.ObjectId
title String
description String
authorId String
authorEmail String
authorName String
user User #relation(fields: [authorId, authorEmail, authorName], references: [id, email, name])
}
This will take the user details and push them, individually, into authorId, authorName and authorEmail. What I would like is some way of loading them into an embedded document called author, like so:
model Post {
id String #id #default(auto()) #map("_id") #db.ObjectId
title String
description String
author Author
user User #relation(fields: [author], references: [id, email, name])
}
type Author {
id String
name String
email String
}
Is this at all possible? I feel like I've tried everything.

Query DynamoDB by GSI using AppSync SDK and Amplify CLI

I'm trying to query a DynamoDB table through AWS AppSync, using AWS'
Android AppSync SDK, and the Amplify CLI. I'm querying a Global
Secondary Index (GSI). I'm getting an error:
Expression block '$[query]' requires an expression
I've found a similar
issue which suggests
the fix below. However, it doesn't work for me:
My schema.graphql is:
type olatexOrders #model
#key(fields: ["PK", "SK"])
#key(name: "GSI-item-1", fields: ["GSIPKitem1", "GSISKitem1" ], queryField: "olatexOrdersByGSIPKitem1AndGSISKitem1")
#key(name: "GSI-order-1", fields: ["GSIPKorder1", "GSISKorder1" ], queryField: "olatexOrdersByGSIPKorder1AndGSISKorder1")
{
PK: String!
SK: String!
GSIPKitem1: String
GSISKitem1: String
GSIPKorder1: String
GSISKorder1: String
... and many more fields not relevant for this case
}
The hash key (primary key + secondary key) and both GSIs (GSI-item-1,
GSI-order1) were created correctly in DynamoDB. I'm also able to query my
DynamoDB table from AppSync using GSI:
query MyQuery {
olatexOrdersByGSIPKorder1AndGSISKorder1(GSIPKorder1: "IN_PROGRESS") {
nextToken
items {
GSIPKorder1
GSISKorder1
}
}
}
However, it doesn't work when I try to use autogenerated amplify classes inside
my Android app, as below:
private void query() {
AwsClientFactory.getInstance(context)
.query(OlatexOrdersByGsipKorder1AndGsisKorder1Query
.builder()
.gSIPKorder1(ORDER_STATUS_IN_PROGRESS)
.limit(200)
.build())
.responseFetcher(AppSyncResponseFetchers.NETWORK_ONLY)
.enqueue(callback);
}
I'm getting the same error mentioned above. After reading the related
issue, my understanding is that there is some
bug/inconsistency/limitation in the way GSIs are implemented in
AppSync and for that reason you need to specify not only the primary key of
the GSI but the sort key and sort order as well. With this knowledge,
for testing, I've rewrite my function to:
private void query() {
AwsClientFactory.getInstance(context)
.query(OlatexOrdersByGsipKorder1AndGsisKorder1Query
.builder()
.gSIPKorder1(ORDER_STATUS_IN_PROGRESS)
.gSISKorder1(ModelStringKeyConditionInput.builder().beginsWith("K").build())
.sortDirection(ModelSortDirection.DESC)
.limit(200)
.build())
.responseFetcher(AppSyncResponseFetchers.NETWORK_ONLY)
.enqueue(callback);
}
Unfortunately, I'm still getting same error:
Expression block '$[query]' requires an expression
I'm using Amplify CLI version 4.27.2.
All help will be appreciated!
EDIT 1
I've tried to simplified my case. I've created GSI having only one column. Please see schema.graphql below:
type olatexOrders #model
#key(fields: ["PK", "SK"])
#key(name: "GSI-item-1", fields: ["GSIPKitem1"], queryField: "olatexOrdersByGSIItem")
#key(name: "GSI-order-1", fields: ["GSIPKorder1"], queryField: "olatexOrdersByGSIOrder")
{
PK: String!
SK: String!
GSIPKitem1: String
GSIPKorder1: String
... and many more fields not relevant for this case
}
Now I'm trying below code to query my dynamo table via Amplify & AppSync:
public class GetInProgressOrders {
private GraphQLCall.Callback<OlatexOrdersByGsiOrderQuery.Data> callback = new GraphQLCall.Callback<OlatexOrdersByGsiOrderQuery.Data>() {
#Override
public void onResponse(#Nonnull Response<OlatexOrdersByGsiOrderQuery.Data> response) {
try{
Log.d("MyTest", "TST response error: "+errors.get(0).message());
}
catch (Exception e){
Log.e("MyTest", e.getMessage())
}
}
#Override
public void onFailure(#Nonnull ApolloException e) {
Log.e("MyTest", e.getMessage())
}
};
private void query(Context context){
AWSAppSyncClient awsClient = AwsClientFactory.getInstance(context);
OlatexOrdersByGsiOrderQuery tmpQuery = OlatexOrdersByGsiOrderQuery
.builder()
.gSIPKorder1(ORDER_STATUS_IN_PROGRESS)
.build();
awsClient.query(
tmpQuery
)
.responseFetcher(AppSyncResponseFetchers.NETWORK_ONLY)
.enqueue(callback);
}
}
Executing above ends up with same error as previously:
TST response error: Expression block '$[query]' requires an expression
That gives me feeling that I'm doing something significantly wrong. Basically I'm unable to query table in Amplify via GSI. Unfortunately I don't see my mistake.
Regards
That will not exactly be the answer but the workaround that actually works.
It turned out, I had multiple issues, some documented better, some worse. Things I did to fix my codes:
For communication from Android App to AWS AppSync use Amplify class instead using AWSAppSyncClient class.
In schema.graphql don't use fields with capital letters only (in my case, instead using PK & SK use pk & sk)
In schema.graphql don't create types that starts with lowercase
Create id column of ID! type (even if you don't need it at all)
With all above being said, see my schema.graphql that acctually works:
type OlatexOrders #model
#key(fields: ["pk", "sk"])
#key(name: "GSI-item-1", fields: ["gsi_pk_item_1", "gsi_sk_item_1"], queryField: "olatexOrdersByGSIItem")
#key(name: "GSI-order-1", fields: ["gsi_pk_order_1", "gsi_sk_order_1"], queryField: "olatexOrdersByGSIOrder")
{
pk: String!
sk: String!
gsi_pk_item_1: String
gsi_sk_item_1: String
gsi_pk_order_1: String
gsi_sk_order_1: String
... different not relevant fields
id: ID!
}
And my query in Android App:
Amplify.API.query(
ModelQuery.list(OlatexOrders.class, OlatexOrders.GSI_PK_ORDER_1.eq(ORDER_STATUS_IN_PROGRESS)),
response -> {
if(response.hasErrors())
Log.i("TestTag", "Errors: " + response.getErrors().get(0));
if(response.hasData()){
for(OlatexOrders orders: response.getData()){
inProgressOrdersNames.add(orders.getGsiSkOrder_1());
}
}
else{
Log.i("TestTag", "No data");
}
},
error -> Log.e("TestTag", "Error", error)
);
Regards!

Create a entity framework LINQ group join query to return a subset of properties from DTO and a list of another DTO, ASP.net core

So, I tried searching and couldn't really find an answer that was explicit enough and guided me to my solution so I thought I would add my problem and ultimately my solution for others to benefit.
Pardon my newness to SO (consider this my start of getting my reputation up), let me know if I do anything incorrect or forget anything.
I am trying to:
Create a controller that queries a database for all the users and their roles.
Return the list of unique users id's and email address with a List of roles.
The email address and roles are in separate tables and the pkey/fkey is the user id
The roles are returned as a list containing my AllUserInRolesDto
Every example I looked at on SO or other sites only provided examples of returning anonymous data objects back. I don't have a lot of LINQ query syntax so had me stuck for an hour or so.
Here is my DTO
namespace Lib.Dtos
{
public class AllUserInRolesDto
{
public string FullName { get; set; }
public string Email { get; set; }
public List<RoleDto> Roles { get; set; }
}
public class RoleDto
{
public string RoleName { get; set; }
}
}
I have a business layer that defines the LINQ query
public List<AllUserInRolesDto> UserAllRolesGet()
{
List<AllUserInRolesDto> getAllUsersRoles = (from u in _context.Users
join r in _context.UserInRoles on u.UserId equals r.UserId
into ur
select new Lib.Dtos.AllUserInRolesDto()
{
FullName = u.Fullname,
Email = u.Email,
Roles = ur //this was the problem line and what the docs were describing
}).ToList();
return getAllUsersRoles;
}
...and my controller
[HttpGet("GetAllUserRolesList")]
public IActionResult GetAllUserRolesList()
{
List<Lib.Dtos.AllUserInRolesDto> allUsers = _userBL.UserAllRolesGet();
return new JsonResult(allUsers);
}
my solution
After taking a step back for a second I realized I actually wasn't returning the right object back to my roles property...and so need to iterate over my roles and create a list from them. Here is what worked.
public List<AllUserInRolesDto> UserAllRolesGet()
{
List<AllUserInRolesDto> getAllUsersRoles = (from u in _Context.Users
join r in _context.UserInRoles on u.UserId equals r.UserId
into ur
select new Lib.Dtos.AllUserInRolesDto()
{
FullName = u.Fullname,
Email = u.Email,
Roles = .Select(x => new Lib.Dtos.RoleDto() { RoleName = x.RoleName }).ToList() //Changed to this
}).ToList();
return getAllUsersRoles;
}
Anyway, probably a pretty dumb mistake, but had me stuck for a bit. Maybe this helps someone in my same position or if someone has a comment of how I could have improved this or used a different approach I am open to hearing suggestions.
I assume you're using Entity Framework, and that you have your DB model defined with relationships. This means you don't need to use explicit JOINs in your queries: you can use navigation properties instead.
Your "business layer" (note that you don't necessarily always need a business layer) should only work with Entity types and should not use DTOs (as DTOs belong to your web-service, in the same way that View-Models belong to a web-application).
If your "business layer" just consists of predefined queries, I recommend defining them as static extension methods for your DbContext and returning IQueryable<T> instead of as materialized List<T> as this enables your consumers to perform further operations on them (such as additional filtering or sorting and paging).
I recommend doing it like this instead:
// Queries methods (i.e. "business layer" queries)
public static class QueriesExtensions
{
public static IQueryable<User> GetAllUsersAndTheirRoles( this MyDbContext db )
{
// I assume `UserInRoles` is a linking table for a many-to-many relationship:
return db
.UserInRoles
.Include( uir => uir.User )
.Include( uir => uir.Role )
.Select( uir => uir.User );
}
}
// Web-service controller:
[HttpGet("GetAllUserRolesList")]
[Produces(typeof(List<AllUserInRolesDto>)]
public async Task<IActionResult> GetAllUserRolesList()
{
List<User> usersList = await this.db
.GetAllUsersAndTheirRoles()
.ToListAsync();
List<Lib.Dtos.AllUserInRolesDto> usersListDto = usersList
.Select( u => ToDto( u ) )
.ToList();
return new JsonResult( usersListDto );
}
// Entity-to-DTO mapping functions (if you have a lot of DTOs and entities, consider using AutoMapper to reduce the tedium)
private static AllUserInRolesDto ToDto( User user )
{
return new AllUserInRolesDto()
{
FullName = user.FullName,
Email = user.Email,
Roles = user.Roles.Select( r => ToDto( r ) ).ToList()
};
}
private static RoleDto ToDto( Role role )
{
return new RoleDto()
{
RoleName = role.RoleName
};
}

Query string parameter vs regular parameter ASP.Net MVC 5

I have been working on desktop applications mostly and thought to learn web development using ASP.Net MVC5 and thus going through the book by Jon Galloway. So I was reading about how you can pass the parameters to action methods using query string like
/Store/Browse?genre=Disco
or directly embed them in the url like
/Store/Details/5
Now the controller code that I wrote (taken from book) is below :
namespace MvcMusicStore.Controllers
{
public class StoreController : Controller
{
// GET: Store
public string Index()
{
return "Hello from Store.Index()";
}
public string Browse(string genre)
{
string message = HttpUtility.HtmlEncode("Store.Browser, Genre = " + genre);
return message;
}
public string Details(int id)
{
string message = "Store.Details, ID = " + id;
return message;
}
}
}
The url opens fine and the actions return the message as expected. But just to try I tried to pass the genre value by embedding it in the url like
/Store/Browse/Rap
but that doesn't work like it did for the Details() action. I thought it may have to do something with the datatype of genre, so I tried changing the data type of id in Details() to string as below :
public string Details(string id)
{
string message = "Store.Details, ID = " + id;
return message;
}
}
and opened the url
/Store/Details/5
and the Details() action returns message with id value 5, but when i do the same for Browse() action
/Store/Browse/Rap
the action doesn't return the message with genre value "Rap". I tried to pass the genre value and removed the html encoding to see if that had anything to do with it, but it didn't.
I looked at the post here but that didn't help either. Any comments appreciated.
Your using the Default route which is defined as
url: "{controller}/{action}/{id}",
and expects a value for id. When you use /Store/Browse/Rap, then the value of the 3rd segment ("Rap") will be bound to a paramater named id, but your Browse() method does not contain one (its named genre).
Either change the name of the parameter so its
public string Browse(string id)
and the value of id will be "Rap",
Or create a specific route definition and place it before the Default route (and keep the Browse() method as is)
routes.MapRoute(
name: "Browse",
url: "Store/Browse/{genre}",
defaults: new { controller = "Store", action = "Browse", genre = UrlParameter.Optional }
);
... // default route here
Side note: You do not need to change the type of the parameter in the Details method if your always passing a value that is a valid int

DropDownListFor "the value is invalid" when setting navigation property

I am attempting to set a navigation property (foreign key) based on the return value from a DropDownList.
I have the following data model:
(Some properties and details omitted for the sake of brevity).
An invite, which has a Guid Id and a collection of guests.
public class Invite
{
public Guid InviteId { get; set; }
public virtual ICollection<Guest> Guests { get; set; }
}
A guest, with an Invite property linking it to invite.
public class Guest
{
public virtual Invite Invite { get; set; }
}
In my DataInitaliser I can correctly build these objects and they are sent to the database, for example:
new Invite()
{
InviteId = Guid.NewGuid(),
Name = "Bloggs",
AllDay = false,
Guests = new List<Guest>()
{
new Guest() { GuestId = Guid.NewGuid(), FirstName = "Joe", LastName = "Bloggs", Vip = false},
}
};
In my GuestController I build the list of possible invites and add it to the ViewBag for presenting in the view.
void PopulateInvite(object invite)
{
var query = db.Invites.Select(i => i).OrderBy(i => i.Name).ToList();
ViewBag.Invites = new SelectList(query, "InviteId", "Name", invite);
}
I present the list of objects in the Guest View like so:
#model Models.Guest
<div class="form-group">
#Html.LabelFor(model => model.Invite, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.Invite, (IEnumerable<SelectListItem>)ViewBag.Invites, String.Empty)
#Html.ValidationMessageFor(model => model.Invite)
</div>
</div>
This correctly displays the expected values from the database.
The problem occurs when I post the values back to the GuestController.
The Post function for the create is pretty much the standard scaffold.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include="GuestId,FirstName,LastName,Vegetarian,Attending,Vip,Invite")] Guest guest)
{
if (ModelState.IsValid)
{
guest.GuestId = Guid.NewGuid();
db.Guests.Add(guest);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
this.PopulateInvite(guest.Invite);
return View(guest);
}
I've dug into the cause a little bit here and I think I understand the underlying problem. My function PopulateInvite, places InviteId into the collection which is a Guid, this is returned as a String (not a Guid?) which cannot be converted into an Invite object.
"The parameter conversion from type 'System.String' to type 'Models.Invite' failed because no type converter can convert between these types."
I did try changing my PopulateInvite collection so its populated with an actual Invite object like so:
void PopulateInvite(object invite)
{
var query = db.Invites.Select(i => i).OrderBy(i => i.Name).ToList().Select(i =>
{
return new
{
Invite = new Invite() { InviteId = i.InviteId },
Name = i.Name
};
});
ViewBag.Invites = new SelectList(query, "Invite", "Name", invite);
}
However this also fails with the same error as above, confusingly I am returned a String representation of the object, instead of the actual object itself.
ModelState["Invite"].Value.RawValue
{string[1]}
[0]: "Models.Invite"
So...what is the correct way to set way to set the navigation property based on the post from the form?
Should I act before ModelState.IsValid to change the Guid into an actual Invite object?
As this tutorial from asp.net suggests, should I add a property to hold an InviteId, instead of using an invite object? In the sample Department is unused so I don't really understand why it has been added - am I missing something?
public class Course
{
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
}
Some other better method?
You can't bind a complex component like model.Invite in a DropDownListFor:
#Html.DropDownListFor(model => model.Invite, (IEnumerable<SelectListItem>)ViewBag.Invites, String.Empty
You need to put a singular value like a int from ID. Try to replace the code above to:
#Html.DropDownListFor(model => model.Invite.InviteID, (IEnumerable<SelectListItem>)ViewBag.Invites, String.Empty
Well, the answer was in the actual tutorial I linked in the question. I needed to add an InviteId field to act as the foreign key then the actual object acts as the navigation property, as explained below both are required (not just one as I was using it).
Creating an Entity Framework Data Model for an ASP.NET MVC Application
The StudentID property is a foreign key, and the corresponding
navigation property is Student. An Enrollment entity is associated
with one Student entity, so the property can only hold a single
Student entity (unlike the Student.Enrollments navigation property you
saw earlier, which can hold multiple Enrollment entities).
The CourseID property is a foreign key, and the corresponding
navigation property is Course. An Enrollment entity is associated with
one Course entity.
Entity Framework interprets a property as a foreign key property if
it's named (for
example, StudentID for the Student navigation property since the
Student entity's primary key is ID). Foreign key properties can also
be named the same simply (for example,
CourseID since the Course entity's primary key is CourseID).

Resources