I have a schema with two object types that link to each other through a field. What is the canonical way to create them in a mutation?
Let's take a simple schema with a User and an Account object type. The simplest case of interdependency is a one-to-one relationship, where a user has exactly one account and an account has exactly one user.
type User {
id: ID!
name: String!
account: Account!
}
type Account {
id: ID!
email: String!
user: User!
}
How would we the mutations to create a user and an account look like?
My first idea was to use separate createUser and createAccount mutations and providing the other object's id. But this doesn't work, since the circular dependency requires the other object to be known.
type Mutation {
createUser(id: ID! name: String! accountId: ID!): User
createAccount(id: ID! email: String! userId: ID!): Account
}
We could make the connection field type in one object optional. Then we could create that object with an empty link to the yet non-existing second object, create the second object (with a link to the first), and then go back to the first and update it with a link to the second.
But I want to avoid making the connection field type optional.
The only way I see is to create both in a single mutation.
type Mutation {
createUserAndAccount(userId: ID! userName: String! accountId: ID! accountEmail: String!): UserAndAccount
}
type UserAndAccount {
user: User
account: Account
}
But now assume we have a one-to-many relationship, where a user can have one or more accounts.
type User {
id: ID!
name: String!
account: [Account!]!
}
type Account {
id: ID!
email: String!
user: User!
}
How would we the mutations to create a user and an account look like now?
When creating a new user, we must create a new account. So, we have to use the previous mutation createUserAndAccount. I'm assuming we don't want to create more than one account initially, and instead go back and make an update mutation to the user when we create additional accounts.
But when creating a new account, the corresponding user might or might not already exist. It might be the first account for a new user, or an additional account for an existing user. We would want a mutation for both possibilities, to either link the account to an existing user or create a new user with it.
The only way I can see, is to add a separate mutation createAccount for creating an additional account for an existing user, while keeping the previous mutation createUserAndAccount for creating an account and a new user.
type Mutation {
createUserAndAccount(userId: ID! userName: String! accountId: ID! accountEmail: String!): UserAndAccount
createAccount(id: ID! email: String! userId: ID!): Account
}
type UserAndAccount {
user: User
account: Account
}
But let's now look at the general case of a many-to-many relationship, where a user can have one or more accounts and an account can have one or more users.
type User {
id: ID!
name: String!
account: [Account!]!
}
type Account {
id: ID!
email: String!
user: [User!]!
}
How would we the mutations to create a user and an account look like now?
When creating either object, the corresponding other object might or might not already exist. Again, I'm assuming we initially don't want to link an object to more than one other object, and instead go back and make an update mutation to it when we create additional other objects.
We would want a mutation for both possibilities, to either link to an existing object or create a new object with it.
The only way I can see is, to keep the createUserAndAccount mutation like previously for creating both objects at the same time, while adding separate mutations when the linked object already exists.
type Mutation {
createUserAndAccount(userId: ID! userName: String! accountId: ID! accountEmail: String!): UserAndAccount
createUser(id: ID! email: String! accountId: ID!): User
createAccount(id: ID! email: String! userId: ID!): Account
}
type UserAndAccount {
user: User
account: Account
}
I recognise this touches on the difficult problem of circular dependencies.
How would you design this API? Would you delete the link in one of the object types to avoid circular dependencies at all cost?
Related
I am creating schemas in GraphQl and testing these in Playground. These seem to work fine in being able to create Club, User and Team, yet I only want to be able to create a team if a club exists and can therefore connect to the club. At the moment I can create a team without a club existing, which of course shouldn't happen. On create Team Mutation, I also want to stop any club being created, the club must exist prior to any mutation on a Team.
Here is what I have at the moment for my schemas
type Club {
id: ID! #unique
name: String! #unique
team: [Team!]! #relation(name: "TeamToClub", onDelete: CASCADE)
admin: User! #relation(name: "UserToClub", onDelete: SET_NULL)
}
type User {
id: ID! #unique
name: String!
team: [Team!]! #relation(name: "TeamToUser", onDelete: SET_NULL)
club: [Club!]! #relation(name: "UserToClub", onDelete: SET_NULL)
}
type Team {
id: ID! #unique
name: String!
club: Club! #relation(name: "TeamToClub", onDelete: SET_NULL)
creator: User! #relation(name: "TeamToUser", onDelete: SET_NULL)
coach:[Coach!]! #relation(name: "CoachToTeam", onDelete: SET_NULL)
}
Here are my mutations for creating a user
mutation {
createUser(
data:{
name:"Jack Jones",
email:"jack#example.com"
}
){
id
name
}
}
and creating a team..
mutation {
createTeam(
data:{
title:"Barcelona FC"
agegroup:"Under 12s",
published: false,
coachcreator:{
connect:{
id:"cka8qkd5h02dj0815m3odzh5s"
}
}
}
){
id
title
agegroup
published
coachcreator {
name
}
}
}
both of which work without connecting to a club.
Could anybody provide a possible solution to ensure a condition is in place to say a team can only be created when a club already exists.
Strange mutations - looks like they are completely not related to types.
Usually create mutation returns created type, f.e. createUser returns User ... and mutation argument - input type (UserInput) - with almost the same shape as returned type (without id).
The simplest method to ensure that createTeam has proper (existing) club relation is to define club as required argument of mutation.
I am trying to create a one-to-many relation of the same type. In this case, a user can report to one user and in turn have many users reporting to them.
My data model currently looks like this:
type User {
id: ID! #id
name: String!
email: String! #unique
reportsTo: User #relation(name: "UserReports")
reports: [User] #relation(name: "UserReports")
}
I expect adding a userId to reportsTo should add the corresponding user's ID to reports.
However, adding a userId to reportsTo is adding the userId to the same user's reports, rather than other users reports.
You can't have a relationship with different elements of the one type. A relation is connecting two different types so we know they share information. Therefore we need to create some addition types that we can base a relationship on.
I've created two different types, one a "supervisor" who is a user who supervises other users, but we will have those users as a second type, "supervisee". Each user can be both a supervisor and a supervisee. There is a relation between a user and each of these two types and a relation between these two types as well.
This is the datamodel:
type User {
id: ID! #id
name: String!
email: String! #unique
supervisor: Supervisor #relation(name: "UserToSupervisor")
supervisee: Supervisee #relation(name: "UserToSupervisee")
}
type Supervisor {
id: ID! #id
user: User! #relation(name: "UserToSupervisor")
supervisees: [Supervisee!]! #relation(name: "SupervisorToSupervisee")
}
type Supervisee {
id: ID! #id
user: User! #relation(name: "UserToSupervisee")
supervisor: Supervisor! #relation(name: "SupervisorToSupervisee")
}
You must view the "supervisor" field in user not as who that users supervior is, but that the user might be a supervisor themselves. This is also the case with supervisee. Basically supervisor and supervisee are extensions of user and the working relationship between them is defined between those two types.
In my Prisma Data Model I started out with a basic User type like this:
type User {
name: String!
email: String! #unique
password: String!
}
Now a User can have two roles: either as a candidate or as a user associated with an employer. If a candidate, the user should also have a set of applications and a set of qualifications, if associated with an employer it should have an access level and a reference to the employer.
First off, is there any way to extend basic types in GraphQL data modelling? If so, how would I go about doing it?
If there is not, I can see three different methods used, and I'm curious what are the pros and cons of each approach:
Having two separate types CandidateUser and EmployerUser, each with the fields name, email, password. I see two problems with this approach: The #unique tag on email is not reliable, and I would have to write a custom verification to make sure the field is unique across both types; and having a single login-function that takes email and fetches the users corresponding data is no longer trivial: it needs to do a lookup in both tables.
Like this:
type CandidateUser {
name: String!
email: String! #unique
password: String!
applications: [Application!]!
qualifications: [Qualification!]!
}
type EmployerUser{
name: String!
email: String! #unique
password: String!
employer: Employer!
accessRight: AccessRight!
}
Again two separate types, but with a RootUser containing name, email and password, and with CandidateUser and EmployerUser each having a one-to-one reference to a RootUser. This would enforce the #unique tag on the email field, but lookup would still be nontrivial.
type RootUser{
name: String!
email: String! #unique
password: String!
}
type CandidateUser {
rootUser: RootUser!
applications: [Application!]!
qualifications: [Qualification!]!
}
type EmployerUser{
rootUser: RootUser!
employer: Employer!
accessRight: AccessRight!
}
Extending User to have the fields within EmployerUser and CandidateUser as optional parameters. This is a pretty simple approach, but I would need custom handling to enforce requiring fields (as in, I can not mark for instance employer as required as that field would not exist for a Candidate).
type User{
name: String!
email: String! #unique
password: String!
applications: [Application!]!
qualifications: [Qualification!]!
employer: Employer
accessRight: AccessRight
}
I really want to ask if there is a better way of solving this. I'm still pretty new to GraphQL and not the best data modeler to begin with, but I'd greatly appraciate any nudge in the right direction :)
And if I do not have any other choice but the three I listed, which one would make the most sense?
What you're trying to do is implementing an interface type:
An Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.
interface User {
name: String!
email: String! #unique
password: String!
}
This means that any type that implements User needs to have these exact fields, with these arguments and return types. So now your Candidate type can implement User:
type Candidate implements User {
name: String!
email: String! #unique
password: String!
applications: [Application!]!
qualifications: [Qualification!]!
}
Interfaces are useful when you want to return an object or set of objects, but those might be of several different types. Have a look at the interface abstract type documentation for more information.
Update:
Since this is a Prisma GraphQL question now, you should be aware that Prisma does not support Interfaces or Union Types as yet. Issue #83 and issue #165 discuss both respectively as feature requests.
However, there is this great article that discuss the workarounds for such approach:
GraphQL Interfaces (and Union Types) with Prisma and Yoga
Which boils down to 2 options:
Storing all data with optional type-specific fields under one type (the interface) in Prisma, and then splitting the data back between the primitive types in the app server.
Storing the data in each primitive type on Prisma, and stitching things for queries on the app server.
I've just started using Prisma. Before was mainly using firebase and mongodb to define my schemas.
I'm trying to define the following schema:
Vote {
id: ID!
from: User! # The user who voted
for: User! # The user that received a vote
rate: Float!
}
Basically, what I want to achieve is enable users to vote for other users (give them a score).
In, say, MongoDB I would do it by creating a separate collection like following:
{
id: DocumentID
from: String // id of the user who voted
for: String // id of the user that received a vote
rate: Number
}
In here I just specify those fields (from and for) as strings and after link them with the User collection by the application logic.
For sure, it's gonna be different in GraphQL Prisma. But I'm still a bit confused on how the relationships are built. And what really happens underneath.
How can I create such schema using Prisma GraphQL?
When there are more than one relational field to the same type, you need to use the #relation directive to make it unambiguous.
type Vote {
id: ID! #unique
votingUser: User! #relation(name: "VoteAuthor")
votedUser: User! #relation(name: "VoteReceiver")
rate: Float!
}
type User {
id: ID! #unique
receivedVotes: [Vote!]! #relation(name: "VoteReceiver")
givenVotes: [Vote!]! #relation(name: "VoteAuthor")
name: String!
}
Suppose I have two types, User and Permission, e.g.
type User {
id: ID!
firstName: String!
lastName: String!
email: String!
permissions: [Permission!]
}
type Permission {
id: ID!
name: String!
}
User has permissions field, which contains a list of associated Permission type records.
How do I structure a mutation query that updates which permissions user has?
I have simply used a list of ID type for the permissions parameter in the mutation
I have set permissions parameter to a list of GraphQLID.
type Mutation {
updateUser (
id: ID!
email: String
password: String
firstName: String
lastName: String
# List of Permission IDs
permissions: [ID!]
): User
}
The problem with this approach is that this does not define relation between Permission type and ID. Therefore, I have added "List of Permission IDs" comment.
Another approach would be to create a dedicated input type, e.g.
input PermissionInput {
id: ID!
}
And use that in instead of ID in the updateUser permissions parameter. However, this does not solve the reference issue.
Unfortunately, I can't add just a comment, but we solve the problem in the same way - but to ensure clarity and consistency of the shape of the data, we use specific id fields. I.e. we would define the input like this:
type Mutation {
updateUser (
id: ID!
email: String
password: String
firstName: String
lastName: String
permissionIds: [ID!]
): User
}
Pretty basic difference, but it clearly differentiates the input from the output, each field name is intuitive and there isn't much overhead.