Properly structuring a GraphQL API that wraps different REST endpoints - ruby

this is my first post on stackoverflow (Long time reader). I mainly come from a Python background and am fairly new to Ruby. I'm wondering on what's the recommended way to structure a Ruby GraphQL API like the following. Each one of the keys has a resolver that requests a different websites API to fetch the related data. These are currently nested under cars in the following way (I removed the actual http sources they are hitting since you need api keys):
Types::CarsType = GraphQL::ObjectType.define do
name "Cars"
field :count, types.ID
field :contact, types.String
field :prices do
type Types::PricesType
resolve ->(obj, args, ctx) {
HTTParty.get('http://example.com').parsed_response.fetch('some_key').map { |data| OpenStruct.new(data) }
}
end
field :inquiries do
type Types::InquiriesType
resolve ->(obj, args, ctx) {
HTTParty.get('http://example1.com').parsed_response.fetch('some_key').map { |data| OpenStruct.new(data) }
}
end
end
Types::InquiriesType = GraphQL::ObjectType.define do
name "Inquiries"
field :name, types.String
field :phone, types.String
end
Types::PricesType = GraphQL::ObjectType.define do
name "Prices"
field :max, types.String
field :min, types.String
field :suggested, types.String
end
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :cars, types.String do
type Types::CarsType
argument :brand, !types[types.String]
resolve ->(obj, args, ctx) {
HTTParty.get('http://example2.com').parsed_response.fetch('some_key').map { |data| OpenStruct.new(data) }
}
end
end
An example query:
query {
cars(brand: "ford") {
count
contact
prices {
max
min
suggested
}
inquiries {
name
phone
}
}
}
This works fine but I feel like the approach isn't using GraphQL to the fullest. Some immediate problems when I look at it is that If you were to make a query like so, it would now make two API requests when only one is needed (the customer is not requesting any fields on cars, just inquiries). Inquiries is an attribute of cars, so I think it makes sense to be nested under cars?
query {
cars(brand: "ford") {
inquiries {
name
phone
}
}
}
I've looked into connections, but I'm not sure if this is the correct use case for them. Is someone with experience in designing solid GraphQL APIs able to weigh in? Should I be trying to leverage unions or interfaces somehow? Thank you so much.

Related

Return custom field based on other not requested field?

Let's say that I want to get a person's age using this query:
{
getUser(id: "09d14db4-be1a-49d4-a0bd-6b46cc1ceabb") {
full_name
age
}
}
I resolve my getUser query like this (I use node.js, type-graphql and knex):
async getUser(getUserArgs: GetUserArgs, fields: UserFields[]): Promise<User> {
// Return ONLY ASKED FIELDS
const response = await knex.select(this.getKnexFields(fields)).from(USER).whereRaw('id = ?', [getUserArgs.id]);
// returns { full_name: 'John Smith' }
return response[0];
}
The problem is that then I can't calculate age field, because I did not get born_at (datetime field stored in a db) in the first place:
#FieldResolver()
age(#Root() user: User, #Info() info: GraphQLResolveInfo): number {
console.log(user); // { full_name: 'John Smith' } no born_at field - no age, so error
// calculate age from born_at
return DateTime.fromJSDate(user.born_at).diff(DateTime.fromJSDate(new Date()), ['years']).years;
}
Is there some fancy graphql-build-in way / convention to predict that born_at will be needed instead of doing it manually through info / context?
You should always return full entity data from the query-level resolvers, so they are available for field resolvers.
The other solution is to manually maintain a list of required fields for field resolvers, so your "fields to knex" layer can always include them additionally".
Further improvements might be to can a list of additional columns based on the requested fields (thus the field resolvers that will be triggered).

How to sort on nested field in graphql ruby?

How do I sort on a nested field (or a virtual attribute) in graphql-ruby?
ExampleType = GraphQL::ObjectType.define do
name 'Example'
description '...'
field :nested_field, NestedType, 'some nested field' do
// some result that is virtually calculated and returns
OpenStruct.new(a: 123//some random number, b: 'some string')
end
end
QueryType = GraphQL::ObjectType.define do
name 'query'
field: example, ExampleType do
resolve -> (_obj, args,_ctx) {
Example.find(args['id']) //Example is an active record
}
end
field: examples, types[ExampleType] do
resolve -> (_obj, args,_ctx) {
// NOTE: How to order by nested field here?
Example.where(args['id'])
}
end
end
And if I am trying to get a list of examples ordered by nested_field.a:
query getExamples {
examples(ids: ["1","2"], order: 'nested_field.a desc') {
nested_field {
a
}
}
}
You can not order Active record by virtual attribute, because Active record can not match this virtual attribute to SQL/NoSQL query. You can avoid limitation, by creating view at DB layer. In GraphQL, sorting/pagination should be implemented at DB layer. Without that sorting/pagination implementation queries all data from DB to application memory.
Also, I want to recommend you switching from order argument with string type to sort argument with [SearchSort!] type based on enums. GraphQL schema will looks like that:
input SearchSort {
sorting: Sorting!
order: Order = DESC
}
enum Sorting {
FieldName1
FieldName2
}
enum Order {
DESC
ASC
}
It helps you implement mapping from GraphQL subquery to DataBase query.

ElasticSearch / NEST 6 - Serialization of enums as strings in terms query

I've been trying to update to ES6 and NEST 6 and running into issues with NEST serializing of search requests - specifically serializing Terms queries where the underlying C# type is an enum.
I've got a Status enum mapped in my index as a Keyword, and correctly being stored in its string representation by using NEST.JsonNetSerializer and setting the contract json converter as per Elasticsearch / NEST 6 - storing enums as string
The issue comes when trying to search based on this Status enum. When I try to use a Terms query to specify multiple values, these values are being serialized as integers in the request and causing the search to find no results due to the type mismatch.
Interestingly the enum is serialized correctly as a string in a Term query, so I'm theorizing that the StringEnumConverter is being ignored in a scenario where it's having to serialize a collection of enums rather than a single enum.
Lets show it a little more clearly in code. Here's the enum and the (simplified) model used to define the index:
public enum CampaignStatus
{
Active = 0,
Sold = 1,
Withdrawn = 2
}
public class SalesCampaignSearchModel
{
[Keyword]
public Guid Id { get; set; }
[Keyword(DocValues = true)]
public CampaignStatus CampaignStatus { get; set; }
}
Here's a snippet of constructing the settings for the ElasticClient:
var pool = new SingleNodeConnectionPool(new Uri(nodeUri));
var connectionSettings = new ConnectionSettings(pool, (builtin, serializerSettings) =>
new JsonNetSerializer(builtin,
serializerSettings,
contractJsonConverters: new JsonConverter[]{new StringEnumConverter()}
)
)
.EnableHttpCompression();
Here's the Term query that correctly returns results:
var singleTermFilterQuery = new SearchDescriptor<SalesCampaignSearchModel>()
.Query(x => x.Term(y => y.Field(z => z.CampaignStatus).Value(CampaignStatus.Active)));
Generating the request:
{
"query": {
"term": {
"campaignStatus": {
"value": "Active"
}
}
}
}
Here's the Terms query that does not return results:
var termsFilterQuery = new SearchDescriptor<SalesCampaignSearchModel>()
.Query(x => x.Terms(y => y.Field(z => z.CampaignStatus).Terms(CampaignStatus.Active, CampaignStatus.Sold)));
Generating the request:
{
"query": {
"terms": {
"campaignStatus": [
0,
1
]
}
}
}
So far I've had a pretty good poke around at the options being presented by the JsonNetSerializer, tried a bunch of the available attributes (NEST.StringEnumAttribute, [JsonConverter(typeof(StringEnumConverter))] rather than using the global one on the client, having an explicit filter object with ItemConverterType set on the collection of CampaignStatuses, etc.) and the only thing that has had any success was a very brute-force .ToString() every time I need to query on an enum.
These are toy examples from a reasonably extensive codebase that I'm trying to migrate across to NEST 6, so what I'm wanting is to be able to specify global configuration somewhere rather than multiple developer teams needing to be mindful of this kind of eccentricity.
So yeah... I've been looking at this for a couple of days now. Good chances there's something silly I've missed. Otherwise I'm wondering if I need to be providing some JsonConverter with a contract that would match to an arbitrary collection of enums, and whether NEST and their tweaked Json.NET serializer should just be doing that kind of recursive resolution out of the box already.
Any help would be greatly appreciated, as I'm going a bit crazy with this one.

How can I do a WpGraphQL query with a where clause?

This works fine
query QryTopics {
topics {
nodes {
name
topicId
count
}
}
}
But I want a filtered result. I'm new to graphql but I see a param on this collection called 'where', after 'first', 'last', 'after' etc... How can I use that? Its type is 'RootTopicsTermArgs' which is likely something autogenerated from my schema. It has fields, one of which is 'childless' of Boolean. What I'm trying to do, is return only topics (a custom taxonomy in Wordpress) which have posts tagged with them. Basically it prevents me from doing this on the client.
data.data.topics.nodes.filter(n => n.count !== null)
Can anyone direct me to a good example of using where args with a collection? I have tried every permutation of syntax I could think of. Inlcuding
topics(where:childless:true)
topics(where: childless: 'true')
topics(where: new RootTopicsTermArgs())
etc...
Obviously those are all wrong.
If a custom taxonomy, such as Topics, is registered to "show_in_graphql" and is part of your Schema you can query using arguments like so:
query Topics {
topics(where: {childless: true}) {
edges {
node {
id
name
}
}
}
}
Additionally, you could use a static query combined with variables, like so:
query Topics($where:RootTopicsTermArgs!) {
topics(where:$where) {
edges {
node {
id
name
}
}
}
}
$variables = {
"where": {
"childless": true
}
};
One thing I would recommend is using a GraphiQL IDE, such as https://github.com/skevy/graphiql-app, which will help with validating your queries by providing hints as you type, and visual indicators of invalid queries.
You can see an example of using arguments to query terms here: https://playground.wpgraphql.com/#/connections-and-arguments

How do I sort by a property on a nullable association in Grails?

I'm trying to sort a table of data. I have the following domain (paraphrased and example-ified):
class Car {
Engine engine
static constraints = {
engine nullable: true // poor example, I know
}
}
class Engine {
String name
}
Here's the controller action that's handling the sort:
def myAction = {
def list = Car.findAll(params)
render(view: 'list', model: [list: list])
}
I provision some data such that there are several Cars, some with null engines and others with engines that are not null.
I attempt the following query:
http://www.example.com/myController/myAction?sort=engine.name&order=asc
The results from the query only return Car entries whose engine is not null. This is different from the results that would be returned if I only queried the association (without its property):
http://www.example.com/myController/myAction?sort=engine&order=asc
which would return all of the Car results, grouping the ones with null engines together.
Is there any way that:
I can get the query that sorts by the association property to return the same results as the one that sorts by only the association (with the null associations grouped together)?
I can achieve those results using the built-in sorting passed to list() (i.e. without using a Criteria or HQL query)
You need to specify LEFT_JOIN in the query, try this:
import org.hibernate.criterion.CriteriaSpecification
...
def list = Car.createCriteria().list ([max:params.max?:10, offset: params.offset?:0 ]){
if (params.sort == 'engine.name') {
createAlias("engine","e", CriteriaSpecification.LEFT_JOIN)
order( "e.name",params.order)
} else {
order(params.sort, params.order)
}
}
Remember to put engine.name as the property to order by in your list.gsp
<g:sortableColumn property="engine.name" title="Engine Name" />

Resources