GraphQL data modelling - extended types (Prisma) - graphql

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.

Related

Graphql - How to include schema from other types

Let us say I have the following type:
type Foo {
id: ID!
field1: String
}
Now, I wish to define another type, which includes the earlier type. Something like this:
type Bar {
...Foo,
field2: String
}
How do I achieve the above in graphql? I want to basically first create a type, and then include that type in the definition of other types so that I don't have to type all the attributes multiple times.
I am using Amplify / AWS Appsync so if there's any special directive that I could use that would also be helpful
GraphQL has the concept interfaces for this. Appsync, AWS's GraphQL implementation, supports interfaces.
[Edit:] GraphQL does not support "...spread" syntax for interfaces. Fields are defined explicitly. Spread syntax does figure in GraphQL, but in the form of Fragments, resuable units of fields for reducing repetition in queries.
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
Amplify, which automagically creates AppSync schemas, resolvers and data sources, is apparently a more difficult story. The amplify-cli repo has an open feature request, Does the GraphQL Transformer support interfaces?. I am no Amplify expert, but a quick look at the loooong feature request comment thread suggests the answer for Amplify is "not out-of-the-box", but "maybe works in narrow circumstances or with advanced customization".

How to create interdependent objects in GraphQL?

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?

Same type relationships in GraphQL / Prisma

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.

GraphQL & Prisma: why does one redefine types in the application schema when they are already part of the Prisma database schema?

Hi — I’ve been following along with a GraphQL/Prisma tutorial (https://www.howtographql.com/graphql-js/6-authentication/) and I’m wondering why one redefines types in the application schema when they are already part of the Prisma database schema and could be imported from there.
The answer the tutorial gives is “To hide potentially sensitive information from client applications”. What does this mean exactly? Why do we replicate definitions in ‘schema.graphql’ and ‘datamodel.prisma’? Because the definitions are slightly different (i.e. the 'datamodel' contains tags like #unique)? And how are we hiding things from client applications? I remain perplexed....
Specifically in ‘schema.graphql’ I have
type User {
id: ID!
name: String!
email: String!
links: [Link!]!
}
and in 'datamodel.prisma' I have
type User {
id: ID! #unique
name: String!
email: String! #unique
password: String!
links: [ Link!] !
}
The schema doesn't have the password field, that is likely what is meant by "hide potentially sensitive information".
This is common practice in any API to not return all data from the persistent storage.

GraphQL Prisma - define "vote" type that links to two users

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!
}

Resources