Alias types in GraphQL Schema Definition Language - graphql

I have the following graphql schema definition in production today:
type BasketPrice {
amount: Int!
currency: String!
}
type BasketItem {
id: ID!
price: BasketPrice!
}
type Basket {
id: ID!
items: [BasketItem!]!
total: BasketPrice!
}
type Query {
basket(id: String!): Basket!
}
I'd like to rename BasketPrice to just Price, however doing so would be a breaking change to the schema because clients may be referencing it in a fragment, e.g.
fragment Price on BasketPrice {
amount
currency
}
query Basket {
basket(id: "123") {
items {
price {
...Price
}
}
total {
...Price
}
}
}
I had hoped it would be possible to alias it for backwards compatibility, e.g.
type Price {
amount: Int!
currency: String!
}
# Remove after next release.
type alias BasketPrice = Price;
type BasketPrice {
amount: Int!
currency: String!
}
type BasketItem {
id: ID!
price: BasketPrice!
}
type Basket {
id: ID!
items: [BasketItem!]!
total: BasketPrice!
}
type Query {
basket(id: String!): Basket!
}
But this doesn't appear to be a feature. Is there a recommended way to safely rename a type in graphql without causing a breaking change?

There's no way to rename a type without it being a breaking change for the reasons you already specified. Renaming a type is a superficial change, not a functional one, so there's no practical reason to do this.
The best way to handle any breaking change to a schema is to expose the new schema on a different endpoint and then transition the clients to using the new endpoint, effectively implementing versioning for your API.
The only other way I can think of getting around this issue is to create new fields for any fields that utilize the old type, for example:
type BasketItem {
id: ID!
price: BasketPrice! # deprecated(reason: "Use itemPrice instead")
itemPrice: Price!
}
type Basket {
id: ID!
items: [BasketItem!]!
total: BasketPrice! # deprecated(reason: "Use basketTotal instead")
basketTotal: Price!
}

I want this too, and apparently we can't have it. Making sure names reflect actual semantics over time is very important for ongoing projects -- it's a very important part of documentation!
The best way I've found to do this is multi-step, and fairly labor intensive, but at least can keep compatibility until a later time. It involves making input fields optional at the protocol level, and enforcing the application-level needs of having "one of them" at the application level. (Because we don't have unions.)
input OldThing {
thingId: ID!
}
input Referee {
oldThing: OldThing!
}
Change it to something like this:
input OldThing {
thingId: ID!
}
input NewThing {
newId: ID!
}
input Referee {
oldThing: OldThing # deprecated(reason: "Use newThing instead")
newThing: NewThing
}
In practice, all old clients will keep working. You can update your handler code to always generate a NewThing, and then use a procedural field resolver to copy it into oldThing if asked-for (depending on which framework you're using.) On input, you can update the handler to always translate old to new on receipt, and only use the new one in the code. You'll also have to return an error manually if neither of the elements are present.
At some point, clients will all be updated, and you can remove the deprecated version.

Related

GraphQL named Object literal typeDef

I'm working through a personal project and I'm trying to figure out the best way to define an Object of Objects that looks like the following in GraphQL's type definitions.
{
"2020-12-29": {
open: true,
hours: 2,
appointments: {
"09:00-am": {
appointmentId: "5223a4ef-a3cf-4e2f-b761-3e06193e2e21",
userName: "Shaun Cartwright Glover",
email: "Maverick.Quitzon31#yahoo.com",
phoneNumber: "1-401-519-4771",
avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/ffbel/128.jpg",
},
},
totalAppointments: 1,
},
As you can see, the name of the Object literal for the top level schedule is the date and the same is being done for each individual appointment. I'm also using graphql prisma if that helps.
To follow along with your example, let's break down your Object literal into the entities that it represents. In high level details, you're looking at
An entity representing an Appointment
The User associated with said Appointment
A list of Appointments, associated with a certain AppointmentTime
A list of AppointmentTimes for a given Day
A list of Days, in the form of a a Schedule
To me, this seems to closely (but not exactly) match the data in the example you have. Based on this, I've defined a schema below, with some intentional design decisions that deviate slightly from your proposal.
Let's define some types, and knit them together, starting with a User type:
type User {
id: ID!
name: String!
email: String!
# Depending on your requirements, a user may not have to provide a phone number.
phoneNumber: String
# Depending on your requirements, a user may not have an Avatar.
avatarUrl: String
}
type Appointment {
id: ID!
user: User!
}
type AppointmentTime {
time: String!
appointments: [Appointment!]!
}
type Day {
# The Day ID could be the actual day itself, i.e. 2020-12-29
id: ID!
open: Boolean!
hours: Int!
appointmentTimes: AppointmentTime!
}
type Schedule {
days: [Day!]!
}
this would allow you to write a query (assuming you have a getSchedule query -- or something to a similar effect) like this:
getSchedule {
days {
id
open
hours
appointmentTimes {
time
appointments {
id
user {
name
email
phoneNumber
avatarUrl
}
}
}
}
}
{
days: [
{
id: "2020-12-29",
open: true,
hours: 2,
appointmentTimes: [
{
time: "09:00-am",
appointments: [
{
id: "5223a4ef-a3cf-4e2f-b761-3e06193e2e21",
user: {
name: "John Smith",
email: "john#smith.com",
phoneNumber: "123...",
avatarUrl: "...",
}
},
...
]
},
...
]
},
...
]
}
Note that this will end up producing a slightly different output than the one you posted. Why?
Well, I made the following design choices, and I'd encourage you to investigate them, too:
A user should be a separate field. In your example, the user and the appointment information are both under the same key -- 09:00-am -- here, we want to leverage GraphQL's type system to normalize the schema by defining a User type that we can attach to an appointment. Better for introspection, too.
Your appointments key points to another object as its value, not a list. Since you're returning a list of appointments at the end of the day, you should model this as a GraphQL List
Added an AppointmentTime type associated with a list of appointments. This allows you to potentially have multiple appointments at the same time. (future proof)
Each day has a list of AppointmentTime --- this is optimal, as you are now no longer dependent on the key (in your case, 09:00-am) to define the data associated with each appointment time. (future proof)
If you really did want the object literal to match the graphql output exactly, you can inline some of the fields I chose to extract to other types, but really, you should be leveraging lists for this kind of thing.

Sorting results in AWS Amplify GraphQL without filtering

Provided a very simple model in graphql.schema, how would I perform a simple sort query?
type Todo #model
id: ID!
text: String!
}
Which generates the following in queries.js.
export const listTodos = /* GraphQL */ `
query ListTodos(
$filter: ModelTodoFilterInput
$limit: Int
$nextToken: String
) {
listTodos(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
text
}
nextToken
}
}
`;
I have found multiple sources pointing me in the direction of the #key directive. This similar question addresses that approach (GraphQL with AWS Amplify - how to enable sorting on query).
While that may seem promising and successfully generates new queries I can use, all the approaches I have tried require that I filter the data before sorting it. All I want to do is sort my todo results on a given column name, with a given sort direction (ASC/DESC).
This is how I would perform a simple (unsorted) query:
const todos = await API.graphql(graphqlOperation(listTodos));
I would be looking to do something along the lines of:
const todos = await API.graphql(graphqlOperation(listTodos, {sortField: "text", sortDirection: "ASC"} )).
Decorate your model with the #searchable directive, like so:
type Todo #model #searchable
{
id: ID!
text: String!
}
After that, you can query your data with sorting capabilities like below:
import { searchToDos } from '../graphql/queries';
import { API, graphqlOperation } from 'aws-amplify';
const toDoData = await API.graphql(graphqlOperation(searchToDos, {
sort: {
direction: 'asc',
field: 'text'
}
}));
console.log(toDoData.data.searchToDos.items);
For more information, see
https://github.com/aws-amplify/amplify-cli/issues/1851#issuecomment-545245633
https://docs.amplify.aws/cli/graphql-transformer/directives#searchable
Declaring #searchable incurs pointless extra server cost if all you need is straight forward sorting. It spins up an EBS and an OpenSearch that will be about $20 a month minumum.
Instead you need to use the #index directive.
As per the documentation here: https://docs.amplify.aws/guides/api-graphql/query-with-sorting/q/platform/js/
In your model, add the #index directive to one of the fields with a few parameters:
type Todo #model {
id: ID!
title: String!
type: String! #index(name: "todosByDate", queryField: "todosByDate", sortKeyFields: ["createdAt"])
createdAt: String!
}
By declaring the queryField and the sortKeyField you will now have a new query available to once you push your amplify config:
query todosByDate {
todosByDate(
type: "Todo"
sortDirection: ASC
) {
items {
id
title
createdAt
}
}
}
The field you declare this directive on can not be empty (notice the ! after the field name)
This is a much better way of doing it as opposed to #searchable, which is massively overkill.
I've accepted MTran's answer because it feels to me it is the nearest thing to an actual solution, but I've also decided to actually opt for a workaround. This way, I avoid adding a dependency to ElasticSearch.
I ended up adding a field to my schema and every single entry has the same value for that field. That way, I can filter by that value and still have the entire table of values, that I can then sort against.

FaunaDB - How to bulk update list of entries within single graphQL mutation?

I want to bulk update list of entries with graphQL mutation in faunaDB.
The input data is list of coronavirus cases from external source. It will be updated frequently. The mutation should update existing entries if the entry name is present in collectio and create new ones if not present.
Current GRAPHQL MUTATION
mutation UpdateList($data: ListInput!) {
updateList(id: "260351229231628818", data: $data) {
title
cities {
data {
name
infected
}
}
}
}
GRAPHQL VARIABLES
{
"data": {
"title": "COVID-19",
"cities": {
"create": [
{
"id": 22,
"name": "Warsaw",
"location": {
"create": {
"lat": 52.229832,
"lng": 21.011689
}
},
"deaths": 0,
"cured": 0,
"infected": 37,
"type": "ACTIVE",
"created_timestamp": 1583671445,
"last_modified_timestamp": 1584389018
}
]
}
}
}
SCHEMA
type cityEntry {
id: Int!
name: String!
deaths: Int!
cured: Int!
infected: Int!
type: String!
created_timestamp: Int!
last_modified_timestamp: Int!
location: LatLng!
list: List
}
type LatLng {
lat: Float!
lng: Float!
}
type List {
title: String!
cities: [cityEntry] #relation
}
type Query {
items: [cityEntry!]
allCities: [cityEntry!]
cityEntriesByDeathFlag(deaths: Int!): [cityEntry!]
cityEntriesByCuredFlag(cured: Int!): [cityEntry!]
allLists: [List!]
}
Everytime the mutation runs it creates new duplicates.
What is the best way to update the list within single mutation?
my apologies for the delay, I wasn't sure exactly what the missing information was hence why I commented first :).
The Schema
An example of a part of a schema that has arguments:
type Mutation {
register(email: String!, password: String!): Account! #resolver
login(email: String!, password: String!): String! #resolver
}
When such a schema is imported in FaunaDB there will be placeholder functions provided.
The UDF parameters
As you can see all the function does is Abort with the message that the function still has to be implemented. The implementation starts with a Lambda that takes arguments and those arguments have to match what you defined in the resolver.
Query(Lambda(['email', 'password'],
... function body ...
))
Using the arguments is done with Var, that means Var('email') or Var('password') in this case. For example, in my specific case we would use the email that was passed in to get an account by email and use the password to pass on to the Login function which will return a secret (the reason I do the select here is that the return value for a GraphQL resolver has to be a valid GraphQL result (e.g. plain JSON
Query(Lambda(['email', 'password'],
Select(
['secret'],
Login(Match(Index('accountsByEmail'), Var('email')), {
password: Var('password')
})
)
))
Calling the UDF resolver via GraphQL
Finally, how to pass parameters when calling it? That should be clear from the GraphQL playground as it will provide you with the docs and autocompletion. For example, this is what the auto-generated GraphQL docs tell me after my schema import:
Which means we can call it as follows:
mutation CallLogin {
login (
email: "<some email>"
password: "<some pword>"
)
}
Bulk updates
For bulk updates, you can also pass a list of values to the User Defined Function (UDF). Let's say we would want to group a number of accounts together in a specific team via the UI and therefore want to update multiple accounts at the same time.
The mutation in our Schema could look as follows (ID's in GraphQL are similar to Strings)
type Mutation { updateAccounts(accountRefs: [ID]): [ID]! #resolver }
We could then call the mutation by providing in the id's that we receive from FaunaDB (the string, not the Ref in case you are mixing FQL and GraphQL, if you only use GraphQL, don't worry about it).
mutation {
updateAccounts(accountRefs: ["265317328423485952", "265317336075993600"] )
}
Just like before, we will have to fill in the User Defined Function that was generated by FaunaDB. A skeleton function that just takes in the array and returns it would look like:
Query(Lambda(['arr'],
Var('arr')
))
Some people might have seen an easier syntax and would be tempted to use this:
Query(Lambda(arr => arr))
However, this currently does not work with GraphQL when passing in arrays, it's a known issue that will be fixed.
The next step is to actually loop over the array. FQL is not declarative and draws inspiration from functional languages which means you would do that just by using a 'map' or a 'foreach'
Query(Lambda(["accountArray"],
Map(Var("accountArray"),
Lambda("account", Var("account")))
))
We now loop over the list but don't do anything with it yet since we just return the account in the map's body. We will now update the account and just set a value 'teamName' on there. For that we need the Update function which takes a FaunaDB Reference. GraphQL sends us strings and not references so we need to transform these ID strings to a reference with Ref as follows:
Ref(Collection('Account'), Var("account"))
If we put it all together we can add an extra attribute to a list of accounts ids as follows:
Query(Lambda(["accountArray"],
Map(Var("accountArray"),
Lambda("account",
Do(
Update(
Ref(Collection('Account'), Var("account")),
{ data: { teamName: "Awesome live-coders" } }
),
Var("account")
)
)
)
))
At the end of the Map, we just return the ID of the account again with Var("account") in order to return something that is just plain JSON, else we would be returning FaunaDB Refs which are more than just JSON and will not be accepted by the GraphQL call.
Passing in more complex types.
Sometimes you want to pass in more complex types. Let's say we have a simple todo schema.
type Todo {
title: String!
completed: Boolean!
}
And we want to set the completed value of a list of todos with specific titles to true. We can see in the extended schema generated by FaunaDB that there is a TodoInput.
If you see that extended schema you might think, "Hey that's exactly what I need!" but you can't access it when you write your mutations since you do not have that part of the schema at creation time and therefore can't just write:
type Mutation { updateTodos(todos: [TodoInput]): Boolean! #resolver }
As it will return the following error.
However, we can just add it to the schema ourselves. Fauna will just accept that you already wrote it and not override it (make sure that you keep the required fields, else your generated 'createTodo' mutation won't work anymore).
type Todo {
title: String!
completed: Boolean!
}
input TodoInput {
title: String!
completed: Boolean!
}
type Mutation { updateTodos(todos: [TodoInput]): Boolean! #resolver }
Which means that I can now write:
mutation {
updateTodos(todos: [{title: "test", completed: true}])
}
and dive into the FQL function to do things with this input.
Or if you want to include the ID along with data you can define a new type.
input TodoUpdateInput {
id: ID!
title: String!
completed: Boolean!
}
type Mutation { updateTodos(todos: [TodoUpdateInput]): Boolean! #resolver }
Once you get the hang of it and want to learn more about FQL (that's a whole different topic) we are currently writing a series of articles along with code for which the first one appeared here: https://css-tricks.com/rethinking-twitter-as-a-serverless-app/ which is probably a good gentle introduction.

How to reference a specific ENUM value in GraphQL type?

I have the following schema:
enum PaymentTypeName {
PAYMENT_CARD
PAYMENT_CARD_TOKEN
}
interface Payment {
id: ID!
type: PaymentTypeName!
}
type PaymentCardPayment implements Payment {
id: ID!
type: PaymentTypeName!
card: PaymentCard!
}
type PaymentCardTokenPayment implements Payment {
id: ID!
type: PaymentTypeName!
card: PaymentCard!
}
When Payment is PaymentCardPayment or PaymentCardTokenPayment is determined by the value of type, i.e. it is either PAYMENT_CARD or PAYMENT_CARD_TOKEN.
How do I signify in the interface, that PaymentCardPayment/ PaymentCardTokenPayment inherit a specific value of PaymentTypeName?
I have tried various combinations of:
type PaymentCardPayment implements Payment {
id: ID!
type: PaymentTypeName.PAYMENT_CARD!
card: PaymentCard!
}
and:
type PaymentCardPayment implements Payment {
id: ID!
type: PaymentTypeName[PAYMENT_CARD]!
card: PaymentCard!
}
but all of these prompt a syntax error and I was unable to find the relevant documentation.
What you're trying to do is not supported in GraphQL. If a field's type is declared to be PaymentTypeName, and PAYMENT_CARD and PAYMENT_CARD_TOKEN are both valid values of PaymentTypeName, then they must also be valid values for that field. There is no way to take an existing type9 whether it's an enum, scalar, or object type) and conditionally create a subset of possible values from the set of possible values already defined by the type.
That said, if PaymentCardPayment.type will always resolve to PAYMENT_CARD and PaymentCardTokenPayment.type will always resolve to PAYMENT_CARD_TOKEN, then it doesn't really make sense to use an enum here at all. In fact, in this specific case, we can omit the type field entirely. After all, the only purpose of the field in this case is to allow the client to distinguish between the possible types that Payment may resolve to. However, GraphQL already provides us with a __typename field that does just that by resolving to the name of the resolved type.
So in this specific instance, it's sufficient to just do:
interface Payment {
id: ID!
}
type PaymentCardPayment implements Payment {
id: ID!
card: PaymentCard!
}
type PaymentCardTokenPayment implements Payment {
id: ID!
card: PaymentCard!
}
and query a field whose type is Payment like this:
{
payments {
id
__typename
... on PaymentCardPayment {
card {
# ...
}
}
... on PaymentCardTokenPayment {
card {
# ...
}
}
}
}
You are trying to declare the field value in your type schema, which is not what a schema is meant for. You should only be declaring your field type within your schema, in this case it is just type: PaymentTypeName. You have it correct in your first code block.
Your PaymentCardPayment's type resolver function should return the value of the enum, in your case, PAYMENT_CARD.
Your PaymentCardTokenPayment's type resolver function should return the value of PAYMENT_CARD_TOKEN.
Edit: This only works on input types.
enum PaymentTypeName {
PAYMENT_CARD
WAD_OF_CASH
}
input BuySomethingInput {
method: PaymentTypeName! = WAD_OF_CASH
price: Number
}
and then use the input type in a Mutation.
The following is not going to work
Would using a default value work for you?
type PaymentCardPayment implements Payment {
id: ID!
type: PaymentTypeName! = PAYMENT_CARD
card: PaymentCard!
}
It won't prevent you from overwriting the value but at least it should be set correctly.

Graphql with nested mutations?

I am trying to figure out how to mutate a nested object with graphql mutations, if possible. For instance I have the following schema:
type Event {
id: String
name: String
description: String
place: Place
}
type Place {
id: String
name: String
location: Location
}
type Location {
city: String
country: String
zip: String
}
type Query {
events: [Event]
}
type Mutation {
updateEvent(id: String, name: String, description: String): Event
}
schema {
query: Query
mutation: Mutation
}
How can I add the place information inside my updateEvent mutation?
Generally speaking, you should avoid thinking of the arguments to your mutations as a direct mapping to object types in your schema. Whilst it's true that they will often be similar, you're better off approaching things under the assumption that they won't be.
Using your basic types as an example. Let's say I wanted to create a new event, but rather than knowing the location, I just have the longitude/latitude - it's actually the backend that calculates the real location object from this data, and I certainly don't know its ID (it doesn't have one yet!). I'd probably construct my mutation like this:
input Point {
longitude: Float!
latitude: Float!
}
input PlaceInput {
name
coordinates: Point!
}
type mutation {
createEvent(
name: String!
description: String
placeId: ID
newPlace: PlaceInput
): Event
updateEvent(
id: ID!
name: String!
description: String
placeId: ID
newPlace: PlaceInput
): Event
)
A mutation is basically just a function call, and it's best to think of it in those terms. If you wrote a function to create an Event, you likely wouldn't provide it an event and expect it to return an event, you'd provide the information necessary to create an Event.
If you want to add a whole object to the mutation you have to define a graphql element of the type input. Here is a link to a small cheatsheet.
In your case it could look like this:
type Location {
city: String
country: String
zip: String
}
type Place {
id: String
name: String
location: Location
}
type Event {
id: String
name: String
description: String
place: Place
}
input LocationInput {
city: String
country: String
zip: String
}
input PlaceInput {
id: ID!
name: String!
location: LocationInput!
}
type Query {
events: [Event]
}
type Mutation {
updateEvent(id: String, name: String, description: String, place: PlaceInput!): Event
}
schema {
query: Query
mutation: Mutation
}

Resources