What are some use cases for adding arguments in a graphql non-query object type? - graphql

As the graphql documentation shows, it's possible to add arguments to a regular object type in your schema.
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
I understand how this is useful for the query type, but not for regular object types. What are some reasons you might want to add arguments to object types?

The first example that comes in my mind is if you have a field such as photo and you may ask for various sizes:
{
me {
id
name
small: photo( scale: 0.5 )
normal: photo
large: photo( scale: 2 )
}
}
Dates with various formats, anything from transformations to filters and whatnot. It's up to you. I'm really crappy with examples, but you get the point.

Related

Interface vs Union in GraphQL schema design

Suppose I am building a GraphQL API that serves a timeline of natural disaster events.
There are two different kinds of event right now:
Hurricane
Earthquake
All events have an ID and a date they occurred. I plan to have a paginated query for fetching events using cursors.
I can think of 2 different approaches to modelling my domain.
1. Interface
interface Event {
id: ID!
occurred: String! # ISO timestamp
}
type Earthquake implements Event {
epicenter: String!
magnitude: Int!
}
type Hurricane implements Event {
force: Int!
}
2. Union
type Earthquake {
epicenter: String!
magnitude: Int!
}
type Hurricane {
force: Int!
}
type EventPayload =
| Earthquake
| Hurricane
type Event {
id: ID!
occurred: String! # ISO timestamp
payload: EventPayload!
}
What are the trade-offs between the two approaches?
I believe that:
unions are about providing: a field / its resolver function resolves with an object, whose type belongs to a specific, known, set of types.
interfaces are about requesting: without, the clients would have to repeat the fields they are interested in, in every type fragment.
They serve different purposes, and they can be used together:
interface I {
id: ID!
}
type A implements I {
id: ID!
a: Int!
}
type B implements I {
id: ID!
b: Int!
}
type C implements I {
id: ID!
c: Int!
}
union Foo = A | C
type Query {
foo: Foo!
}
This schema declares that A, B, and C have some fields in common, so that it's easier for the client to request them, and that querying foo can only yield A or C.
Could you write foo: I! instead? While this would work seamlessly, I believe this leads to a bad development experience. If you're saying that foo provides an I object, your clients should be prepared for receiving any of the implementing types, including B, and would spend time to write and maintain a code that will never be called. If you know that foo can only yield A and C, please tell them explicitly.
The same holds if foo were to yield A, B, or C. It happens that it's exactly the list of types that implement I, so in this case, could you write foo: I!? No! Don't be fooled by that. Why? Because this list is expandable through federation / schema stitching! I believe it's a seldom used feature of some GraphQL frameworks, but whose adoption grows. If you've never used it, please try, it will open your mind to new ideas of inter-micro-service-communication and other Medium buzzwords. In short, imagine you're making a public API, or even somewhat-public within an organization. Someone else could "augment" your API by providing extra stuff. This may include new types implementing your interface. And so we're back to the previous paragraph.
So far, it looks like I'm in favor of your first code.
However, and this may be specific to this scenario, it seems to me that your definition of event mixes both data about its occurrence and about physics metrics. Your second code splits them into two type hierarchy. I like that. It feels more architecture-friendly. Your schema is more open. Imagine your API is about event history, and someone enhance it with forecasts: your EventPayload can be reused!
Besides, note that your first example is incomplete. Types implementing an interface must implement, i.e. repeat, every single field of this interface, like I wrote in the above code. This becomes harder to maintain as the number of fields and the number of implementing types grow.
So, the second solution also has some advantages. But doing so, the blah-blah I made earlier about being specific with returned types is hard to implement, because the payload, which is the one to be specific about, is embedded into another type, and there's no such thing as generics in GraphQL.
Here's a proposal to reconcile all of that:
interface HasForce {
force: Int!
}
type Earthquake {
epicenter: String!
magnitude: Int!
}
type Hurricane implements HasForce {
force: Int!
}
type Tsunami implements HasForce {
force: Int!
}
interface Event {
data: EventData!
}
type EventData {
id: ID!
occurred: String!
}
union HistoryMeteorologicalPhenomenon = Earthquake | Hurricane
type HistoryEvent implements Event {
data: EventData!
meteorologicalPhenomenon: HistoryMeteorologicalPhenomenon!
}
type Query {
historyEvents: [HistoryEvent!]!
}
It looks a bit more complex that both of your proposals, but it fulfills my needs. Also, it's rare to look at a schema from this height: more often, we know the entry point and dig down from there. For instance, I open the documentation at historyEvents, see that it yields phenomena of two kinds, fine, I'm not aware that other union types and event types exist.
If you were to write a lot of these union + event pairs, you could generate them with code instead, whereby one function call would declare a pair. Less error-prone, funnier to implement, and with more potential of Medium articles.
Note that the GraphQL structure is independent of your storage structure. It's possible to have multiple GraphQL objects providing data from the same insert-your-language-here object, e.g. yielded by your DB driver. There may be a tiny overhead that I haven't benchmarked, but providing a cleaner API outweighs that to me. The basic idea is that resolver functions just have to resolve with the same source, so that the resolver functions related to another type will be called with the same source object.

Can one combine two types to make a third in GraphQL schema syntax?

I have a feeling this will be deemed Not How You Do It In GraphQL, but I'm pretty new to it, so please be patient and verbose with me.
Let's say I've got two GraphQL types that I'd like to be able to utilize separately:
type UserSpecs {
name: String!
age: Int!
bio: String!
}
type UserCollections {
interests: [Interest]
buddies: [Relationship]
chats: [Chat]
}
type Query {
updateCollections(collections: UserCollections): User
updateUserSpecs(specs: UserSpecs): User
}
In my .gql file, I'd like to also define the User type as the combination of UserSpecs and UserCollections, though. In TypeScript, for instance, one would do this:
type User = UserSpecs & UserCollections
Short of manually duplicating the contents of UserSpecs and UserCollections into a third type, which would not be DRY and would create two sources of truth to maintain, does the GraphQL schema syntax have any way of combining two types to make a third?
Similarly, if it's possible to create a User type, then disassemble it into the UserSpecs and UserCollections types I'm after, that would be equally helpful.
Thank you in advance!

I need a type for a graphql property that could be two 2 types

So I have some code like the following:
input Data {
activityValue: Int
}
But I need it to be something more like
input Data {
activityValue: Int | String!
}
I know in typescript, even though frowned upon you can use any or number | string. Is there anything like this in graphql?
There is no real such thing as multiple types in the GraphQL specification. However, Unions can fit your needs.
From the specification:
GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types.
That means that Unions can include types but no scalars or lists.
For example, a union can be declared like this:
union Media = Book | Movie
And then be used as a type:
type Query {
allMedia: [Media] # This list can include both Book and Movie objects
}
Example is taken from Apollo Docs.
If you want to check in your query if you have some type of the Union type, then you need to do that with inline fragments.
query Test {
singleMedia(id: 123) {
name
... on Book {
author
}
... on Movie {
musicTitle
}
}
}

filter by Time in graphql (using faunaDB service)

My graphQL schema looks like this,
type Todo {
name: String!
created_at: Time
}
type Query {
allTodos: [Todo!]!
todosByCreatedAtFlag(created_at: Time!): [Todo!]!
}
This query works.
query {
todosByCreatedAtFlag(created_at: "2017-02-08T16:10:33Z") {
data {
_id
name
created_at
}
}
}
Could anyone point out how i can create greater than (or less than) Time query in graphql (using faunaDB).
GraphQL range queries are not supported (yet.. they're coming!)
FaunaDB does not provide range queries for their GraphQL out-of-the-box, we are working on these features.
.. but there is a workaround.
That doesn't mean though that it can't do range queries since range queries are supported in FQL and you can always 'escape' from GraphQL to FQL to implement more advanced queries by writing a User Defined Function (UDF).
.. using resolvers
By using the #resolver keyword in your schema you can implement GraphQL queries yourself by writing a User Defined Function in FaunaDB in FQL. There are some basic examples in the documentation bt I imagine you might need some help so I'll write you a simple example.
I added your schema and added two documents:
First thing is that our schema will be extended with the resolver:
type Todo {
name: String!
created_at: Time
}
type Query {
allTodos: [Todo!]!
todosByCreatedAtFlag(created_at: Time!): [Todo!]!
todosByCreatedRange(before: Time, after:Time): [Todo!]! #resolver
}
All this does is add a function for us to implement:
Which if we call via GraphQL gives us exactly that Abort message we saw in the screenshot before since it has not been implemented yet. But we can see that the GraphQL statement actually calls the function.
.. UDF implementation
First thing we will do is add the parameter which is just writing a name as the first parameter of the lambda:
Which also takes an array in case you need to pass multiple parameters (which I do in the resolver that I defined in the schema):
We'll add an index to support our query. Values are for ranges (and for return values and sorting). We'll add created_at to range over it and also add ref since we'll need the return value to get the actual document behind the index.
We could then start off by just writing a simple function (that won't work yet)
Query(
Lambda(
["before", "after"],
Paginate(
Range(Match(Index("todosByCreatedAtRange")), Var("before"), Var("after"))
)
)
)
and could test this by calling the function manually via the shell.
This indeed returns the two objects (range is inclusive).
Of course, there is one problem with this, it does not return the data in the structure that GraphQL expects it so we'll get these strange errors:
We can do two things now, either define a type in our Schema that fits these and/or we can adapt the data the returns. We'll do the latter and adapt our result to the expected [Todo!]! result to show you.
Step one, map over the result. The only thing we introduce here is the Map and the Lambda. We do not do anything special yet, we just return the reference instead of both the ts and the reference as an example.
Query(
Lambda(
["before", "after"],
Map(
Paginate(
Range(
Match(Index("todosByCreatedAtRange")),
Var("before"),
Var("after")
)
),
Lambda(["created_at", "ref"], Var("ref"))
)
)
)
Calling it indeed shows that the function now only returns references.
Let's get the actual documents. I know that FQL is verbose (and with good reasons, although it should become less verbose in the future) so I started adding comments to clarify things
Query(
Lambda(
["before", "after"],
Map(
// This is just the query to get your range
Paginate(
Range(
Match(Index("todosByCreatedAtRange")),
Var("before"),
Var("after")
)
),
// This is a function that will be executed on each result (with the help of Map)
Lambda(["created_at", "ref"],
// We'll use Let to structure our queries ( allowing us to use varaibles )
Let({
todo: Get(Var("ref"))
},
// And then we return something
Var("todo")))
)
)
)
Our function now returns data.. woohoo!
We still need to make sure this data is conforms to what GraphQL expects, and from the schema we can see that it expects a [Todo!]! (See docs tab) and a Todo looks like (see the schema tab):
type Todo {
_id: ID!
_ts: Long!
name: String!
created_at: Time
}
As you can also see from that docs tab is that 'non-resolver' queries are automatically changed to return TodoPages. The function we wrote so far actually return pages.
Option 1, change the schema and turn it into a paginated resolver.
We can fix this by adding the paginated: true option to the resolver. You will have to take into account for extra parameters that will be added to the resolver as explained here. I haven't tried that myself, so I'm not 100% certain how that would work. The advantage of a paginated resolve is that you can immediately take advantage of sane pagination in the GraphQL endpoint.
Option 2, turn it into a non-paginated result.
A paginated result is a result that looks as follows:
{ data: [ document1, document2, .. ],
before: ...
after: ..
}
The result doesn't accept pages but an array so I'll change it and retrieve the data field:
And we have our result.
The complete query looks as follows:
Query(
Lambda(
["before", "after"],
Select(
["data"],
Map(
Paginate(
Range(
Match(Index("todosByCreatedAtRange")),
Var("before"),
Var("after")
)
),
Lambda(
["created_at", "ref"],
Let({ todo: Get(Var("ref")) }, Var("todo"))
)
)
)
)
)
Disclaimers
Once you go custom, pagination also becomes your responsibility (e.g. pass an extra parameter). You can't fetch relations out of the box anymore as you would normally do by just requesting the relations in the GraphQL body.
Some words on the benefits of UDFs and the hybrid of GraphQL/FQL
Before you shy away from FQL (and yes, we do have to add range queries and are working on that), here is some explanation on the UDF approach in general and why it makes sense to think about it anyway.
You will at a certain moment encounter things in GraphQL that are just impossible (complex conditional transactions, e.g. update document and update this other document only if some condition that results form the previous update is true). Users that use other GraphQL implementations typically solve this by writing a serverless function in case you have to implement advanced logic or transactions.
FaunaDB's answer to this is to use their User Defined Functions (UDFs). This is not a serverless function, it's a FaunaDB function implemented in FQL which might seem cumbersome at first but it's important to realize that it gives you the same benefits ( multi-region/strong consistency/scalability/free-tier/pay-as-you-go) that FaunaDB provides.

Is it a bad practice to use an Input Type for a graphql Query?

I have seen that inserting an Input Type is recommended in the context of mutations but does not say anything about queries.
For instance, in learn tutorial just say:
This is particularly valuable in the case of mutations, where you might want to pass in a whole object to be created
I have this query:
type query {
person(personID: ID!): Person
brazilianPerson(rg: ID!): BrazilizanPerson
foreignerPerson(passport: ID!): ForeignerPerson
}
Instead of having a different type just because of the name (rg, passport) of the fields, or put one more argument like type in query, I could not just have the Person with an documentNr field and do an Input type like that?
input PersonInput {
documentNr : ID!
type: PersonType # this type is Foreign or Brazilian and with this I k
}
PersonType is a enum and with him I know if the document is a rg or a passport.
No, there is nothing incorrect about your approach. The GraphQL spec allows any field to have an argument and allows any argument to accept an Input Object Type, regardless of the operation. In fact, the differences between a query and a mutation are largely symbolic.
It's worth pointing out that any field can accept an argument -- not just ones at the root level. So if it suited your needs, you could easily set up a schema that would allow queries like:
query {
person(id: 1) {
powers(onlyMutant: true) {
name
}
}
}

Resources