Introduction
If you are reading this article, you are probably familiar with GraphQL and TypeScript (and perhaps Prisma). However, I doubt that many of you have heard of PlanetGraphQL.
PlanetGraphQL is a library aimed at quickly creating feature-rich and flexible GraphQL APIs when used in combination with TypeScript and Prisma. In this tutorial, I hope to give you an overview of PlanetGraphQL by creating a GraphQL API for a simple TODO app.
I assume readers have a basic understanding of Node.js and TypeScript. I think that there are some of you who have never actually used GraphQL or Prisma, so I will explain these in the tutorial along with PlanetGraphQL.
The full source code can be found in this repository
Creating a project
node.js (v16 or higher) must be installed
First, create an empty TypeScript project.
mkdir tutorial && cd tutorial
npm init --yes
npm install typescript ts-node --save-dev
Next, create tsconfig.json and configure as follows:
touch tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"lib": ["es2021"],
"module": "commonjs",
"target": "es2021",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Add Prisma and PlanetGraphQL to your dependencies.
npm install prisma @planet-graphql/core
Prepare the Prisma environment with the Prisma initialization command.
npx prisma init --datasource-provider sqlite
This time, we will use sqlite to simplify the explanation and environment construction.
Of course, you can also use PostgreSQL or MySQL.
Defining the DB schema
In this tutorial, we are going to create an API for a simple TODO application with the following functionality:
- Users can create and edit their own tasks
- Users can list their own tasks
- In addition to the above, admin users can list all users and tasks for each user
This time, let's simply define the User and Task tables.
In Prisma, model definitions are described in the schema.prisma
file.
Please add the following contents to prisma/schema.prisma
.
// ...
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
tasks Task[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id Int @id @default(autoincrement())
title String
content String?
status String
dueAt DateTime
user User @relation(fields: [userId], references: [id])
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Creation and execution of migration files
Prisma has a DB schema migration feature.
Let's create a migration file with the following command.
npx prisma migrate dev --name init
If the command succeeds, the file prisma/migrations/${yyyyMMddHHmmss}_init/migration.sql
will be created. This contains the SQL generated by Prisma. At the same time the SQL file is created, it is also applied to the DB, and the User table and Task table are created.
Adding a generator for PlanetGraphQL
Generator is the feature of Prisma. By using a generator, you can generate the corresponding JSON Schema or TypeScript type definition based on the model definition described in schema.prisma
.
If you look at schema.prisma
again, you will notice the following description:
generator client {
provider = "prisma-client-js"
}
This is the generator definition for generating the type definitions needed by the PrismaClient
to call Prisma from TypeScript.
PlanetGraphQL also needs a generator to generate the type definition for TypeScript, the same as PrismaClient. Add the following definition to schema.prisma
.
generator pg {
provider = "planet-graphql"
}
After adding it, execute the following command.
Any generators configured in schema.prisma
will be re-run to generate the type definitions required by PlanetGraphQL.
npx prisma generate
Setting up GraphQL API server
Now that the Prisma side is ready, let's implement a GraphQL API server using PlanetGraphQL.
First, convert the Prisma model to a GraphQL object type using PlanetGraphQL. Create an index.ts
file and fill it as follows.
touch index.ts
import { dmmf, getPGBuilder, getPGPrismaConverter, PrismaTypes } from '@planet-graphql/core'
const pg = getPGBuilder<{ Prisma: PrismaTypes }>()
const pgpc = getPGPrismaConverter(pg, dmmf)
const { objects } = pgpc.convertTypes()
The pgpc.convertTypes
method return value objects
contains User and Task, which are GraphQL object types corresponding to Prisma models. Let's use objects.User
in it to define a usersQuery
that returns a list of users. Please add the following contents to index.ts.
import { PrismaClient } from '@prisma/client'
// ...
const prisma = new PrismaClient({ log: ['query'] })
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
.resolve(() => prisma.user.findMany())
})
Here we implement the following:
- Get PrismaClient instance with
new PrismaClient()
- Defines a GraphQL query with the
pg.query
method -
b.object(() => objects.User)
defines that the return type of UsersQuery is User -
list()
changes the return type to List type of User -
resolve(() => prisma.user.findMany())
defines what to do when UsersQuery is actually called. Here, PrismaClient is used to retrieve all user records that exist in the DB.
Let's start up the GraphQL API server now. Since PlanetGraphQL is just a GraphQL Schema Builder, a separate GraphQL Server is required to launch it as an API server. In this tutorial, we will use graphql-yoga
, the GraphQL Server that is said to be trendy these days. Of course, you can also use other libraries such as apollo-server
, which is the most popular and well-known.
npm install @graphql-yoga/node
Also add the following to index.ts
.
import { createServer } from '@graphql-yoga/node'
// ...
const server = createServer({
schema: pg.build([usersQuery]),
maskedErrors: false,
})
server.start()
Let's add the startup script to package.json and run it.
{
"scripts": {
+ "start": "ts-node index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
}
npm run start
Accessing http://localhost:4000/graphql
opens an IDE for developers called graphiql
. Let's call the usersQuery we just implemented on graphiql. Write the following query in the left pane of graphiql and press the execute button.
query {
users {
id
name
}
}
Since the DB is empty, the following results would be returned.
{
"data": {
"users": []
}
}
Adding records to DB
If the DB is empty, it is not possible to sufficiently check the operation, so let's create a seed script to put data into the DB. You can execute SQL directly, but since this is a good opportunity, let's create data via PrismaClient. We will install facker.js
for creating test data.
npm install @faker-js/faker --save-dev
Create a seed.ts
and add a script that creates users and tasks for each user.
touch seed.ts
import { PrismaClient } from '@prisma/client'
import { faker } from '@faker-js/faker'
const prisma = new PrismaClient()
async function clean() {
await prisma.task.deleteMany()
await prisma.user.deleteMany()
}
async function createUsersAndTasks(userCount = 100, taskCount = 10) {
for (let userId of [...Array(userCount).keys()]) {
const firstName = faker.name.firstName()
await prisma.user.create({
data: {
id: userId,
name: faker.name.fullName({ firstName }),
email: faker.internet.email(firstName),
tasks: {
create: [...Array(taskCount).keys()].map((taskId) => ({
id: userId * userCount + taskId,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
status: faker.helpers.arrayElement(['new', 'in_progress', 'done']),
dueAt: faker.date.future()
}))
}
}
})
}
}
async function seed() {
await clean()
await createUsersAndTasks()
}
seed()
Prisma provides the function to call the seed command set in package.json with prisma db seed
. Please add the following settings to package.json and execute the command.
{
"scripts": {
"start": "ts-node index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
+ "prisma": {
+ "seed": "ts-node seed.ts"
+ },
"keywords": [],
}
npx prisma db seed
Start up the API server again and run the previous query.
If the seed is successful, you should get a list of users this time.
npm run start
query {
users {
id
name
}
}
Getting Relations
Let's change the previous query so that we can also retrieve tasks for each user at one time.
query {
users {
id
name
tasks {
id
title
}
}
}
When you call the above query, you should get an error Cannot return null for non-nullable field User.tasks
. Let's change the implementation of usersQuery slightly to eliminate this error.
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
- .resolve(() => prisma.user.findMany())
+ .resolve(() => prisma.user.findMany({
+ include: { tasks: true }
+ }))
})
Restart the server and run the query again, this time you should also get all per-user tasks. By passing { include: { tasks: true } }
, the tasks associated with the user are also retrieved from the DB.
We would like to solve all problems with this, but there is a performance problem with always passing { include: { tasks: true } }
. This is because tasks are selected from the DB even when they are not needed. Ideally, you should pass { include: { tasks: true } }
when you need to retrieve tasks, and not pass it when you don't.
Let's solve this problem using PlanetGraphQL's prismaArgs
. let's change the implementation of usersQuery a little bit.
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
- .resolve(() => prisma.user.findMany({
- include: { tasks: true }
- }))
+ .resolve(({ prismaArgs }) => prisma.user.findMany(prismaArgs))
})
You should still get tasks for each user after the change. The prismaArgs
received by the resolve
method callback contains an include clause automatically generated from the GraphQL query.
If your query is like this:
query {
users {
id
name
tasks {
id
title
}
}
}
The value of prismaArgs will be { include: { tasks: true } }
. On the other hand, if the tasks field is not specified as follows:
query {
users {
id
name
}
}
The value of prismaArgs will be empty.
Filtering and Sorting
Prisma has filtering and sorting features similar to SQL. For example, if you want to retrieve users whose name contains "e", you can write:
const users = await prisma.user.findMany({
where: {
name: { contains: "e" }
}
})
If you want to retrieve the 11th through 20th user, you can write:
const users = await prisma.user.findMany({
take: 10,
skip: 10,
})
Let's use PlanetGraphQL to add the offset-based pagination feature to usersQuery. Modify the usersQuery as follows.
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
+ .args((b) => ({
+ take: b.int().default(10),
+ skip: b.int().default(0),
+ }))
- .resolve(({ prismaArgs }) => prisma.user.findMany(prismaArgs))
+ .resolve(({ args, prismaArgs }) => prisma.user.findMany({
+ ...prismaArgs,
+ take: args.take,
+ skip: args.skip,
+ }))
})
After restarting the server, you should be able to call usersQuery passing any take
and skip
as follows:
query {
users(take: 10, skip: 10) {
id
name
}
}
Let's optimize the previous implementation slightly. Modify it as follows:
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
- .args((b) => ({
+ .prismaArgs((b) => ({
take: b.int().default(10),
skip: b.int().default(0),
}))
- .resolve(({ args, prismaArgs }) => prisma.user.findMany({
- ...prismaArgs,
- take: args.take,
- skip: args.skip,
- }))
+ .resolve(({ prismaArgs }) => prisma.user.findMany(prismaArgs))
})
You should be able to call call usersQuery by passing any take and skip as before.
Using the prismaArgs
method instead of the args
method simplified the implementation a bit. Whether you use the args
method or the prismaArgs
method, the generated GraphQL schema stay s the same. The only difference is whether it is passed as args
or prismaArgs
at resolve time.
For values that you want to pass directly to PrismaClient, it may be simpler to use the prismaArgs
method.
Introducing PlanetGraphQL ArgBuilder
In the previous example, the args were simple, such as take and skip, so you could define those by yourself. But what if you want to define more complex args? In such a case, the PGArgBuilder
provided by PlanetGraphQL is very useful.
Try changing the usersQuery as follows and restart the server.
+ const { args } = pgpc.convertBuilders()
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
- .prismaArgs((b) => ({
- take: b.int().default(10),
- skip: b.int().default(0),
- }))
+ .prismaArgs(() => args.findManyUser.build())
.resolve(({ prismaArgs }) => prisma.user.findMany(prismaArgs))
})
Then a bunch of args will be added to usersQuery. For example, it becomes possible to call usersQuery as follows:
query {
users(
where: {
email: { contains: "e" },
tasks: { some: { status: { equals: "done" } } }
},
orderBy: [ { updatedAt: desc }, { id: desc } ],
take: 10,
) {
id
name
tasks {
status
}
}
}
By calling the pgpc.convertBuilders
method, you can receive PGArgBuilders corresponding to PrismaClient's findManyUser
, findUniqueTask
, etc. By using PGArgBuilder, it is possible to convert PrismaClient's findManyUser
or findUniqueTask
to GraphQL args.
Additionally, PGArgBuilder can adjust its contents using the edit
method. For example, let's edit as follows:
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
.auth(({ context }) => context.isAdmin, { strict: true })
- .prismaArgs(() => args.findManyUser.build())
+ .prismaArgs(() => args.findManyUser.edit((f) => ({
+ where: f.where.edit((f) => ({
+ email: f.email.select('StringFilter').edit((f) => ({
+ equals: f.equals,
+ contains: f.contains,
+ })),
+ tasks: f.tasks.edit((f) => ({
+ some: f.some.edit((f) => ({
+ status: f.status
+ .select('String')
+ .validation((schema) => schema.regex(/new|in_progress|done/)),
+ }))
+ })),
+ })),
+ orderBy: f.orderBy,
+ take: f.take.default(10),
+ skip: f.skip.default(0),
+ })).build({ type: true }))
.resolve(({ prismaArgs }) => prisma.user.findMany(prismaArgs))
Here, we have made the following adjustments:
- Only use the part of the args provided in findManyUser that we think is acceptable to expose as GraphQL API
- Validation is set so that values other than
new
,in_progress
, anddone
are not accepted in the task status filtering conditions - Set default values for
take
andskip
As you can see, you can easily implement sophisticated filtering and sorting functions as a GraphQL API. It is also nice that type completion works and you can fully benefit from TypeScript.
Customizing Object Types
So far we have used objects.User
generated from Prisma's schema as is, but let's customize this User. Please add the following contents to index.ts.
+ const user = pgpc.redefine({
+ name: 'User',
+ fields: (f, b) => ({
+ ...f,
+ taskCount: b.int()
+ }),
+ relations: () => getRelations('User'),
+ })
+ const task = pgpc.redefine({
+ name: 'Task',
+ fields: (f) => {
+ const { user, ...rest } = f
+ return { ...rest }
+ },
+ relations: () => getRelations('Task'),
+ })
+ user.implement((f) => ({
+ taskCount: f.taskCount.resolve((params) => {
+ const user = params.source
+ return prisma.task.count({
+ where: {
+ userId: user.id,
+ }
+ })
+ })
+ }))
- const { objects } = pgpc.convertTypes()
+ const { objects, getRelations } = pgpc.convertTypes({
+ User: () => user,
+ Task: () => task,
+ })
This might seem complicated. I will try to explain it one by one.
- Redefining User and Task with
pgpc.redefine
method- In User, we added an Int type field
taskCount
to get the number of tasks the user has - The
user
field is removed from Task - The
relations: () => getRelations('User')
part will be explained later
- In User, we added an Int type field
- Implement the
taskCount
field using theuser.implement
method- For fields that do not exist in the DB, it is necessary to set the resolver and implement how to return the value
- In this case, the count method of PrismaClient is used to get the number of tasks for the target user
- The redefined User and Task are passed to the
pgpc.convertTypes
method in the form of{ User: () => user, Task () => task }
- By doing this, the returned values
objects.User
andobjects.Task
will be the redefined User and Task - The
getRelations
method and therelations
field in theredefine
method are needed to replace the object type of relations with redefined object types. - In this example, it is necessary to replace the Task in the User's
tasks
field with the redefined Task
- By doing this, the returned values
In summary, the redefine method allows you to add and remove fields from Prisma's schema definition. Please restart the server and make sure you can get taskCount from the usersQuery.
query {
users {
id
name
taskCount
}
}
Resolving the N+1 problem by introducing DataLoader
By adding the taskCount field, we can now get the number of tasks per user. However, there is one problem. The SQL is not optimized and the Select statement to retrieve the number of tasks is being issued for the number of users. This is the so-called N+1 problem.
GraphQL uses a mechanism called DataLoader to solve the N+1 problem, and PlanetGraphQL has built-in DataLoader functionality, so you can easily use dataloader. Modify index.ts as follows:
user.implement((f) => ({
taskCount: f.taskCount.resolve((params) =>
pg.dataloader(params, async (userList) => {
- const user = params.source
- return prisma.task.count({
- where: {
- userId: user.id,
- }
- })
+ return pg.dataloader(params, async (userList) => {
+ const userIds = userList.map((x) => x.id)
+ const resp = await prisma.task.groupBy({
+ by: ['userId'],
+ _count: { _all: true },
+ where: { userId: { in: userIds } },
+ })
+ return userIds.map((id) => resp.find((x) => x.userId === id)?._count._all ?? 0)
+ })
)
}))
Try restarting the server and retrieving taskCount from usersQuery. It is clear from the logs that the SQL that was issued for each user has changed to a single SQL as shown below.
SELECT COUNT(*), `main`.`Task`.`userId` FROM `main`.`Task` WHERE `main`.`Task`.`userId` IN (?,?,?,?,?,?,?,?,?,?) GROUP BY `main`.`Task`.`userId` LIMIT ? OFFSET ?
Introducing Relay
In GraphQL, the most common implementation of pagination is a cursor-based implementation called Relay-style. While cursor-based pagination is easy to use, it can be difficult to implement. However, with the combination of Prisma and PlanetGraphQL, it is very easy to create an API that supports the Relay-style.
Let's try to implement an API to obtain a list of tasks in the form of Relay-style. Please add the following to your index.ts.
const tasksQuery = pg.query({
name: 'tasks',
field: (b) =>
b
.object(() => objects.Task)
.relay()
.resolve(({ prismaArgs }) => prisma.task.findMany(prismaArgs))
})
Also, add an argument to the pg.build
method so that the newly defined tasksQuery
is included in the schema.
const server = createServer({
- schema: pg.build([usersQuery]),
+ schema: pg.build([usersQuery, tasksQuery]),
maskedErrors: false,
})
This is all that is needed to call tasksQuery in Relay-style.
query {
tasks(first: 10) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
You can make a Query or field correspond to Relay-style by simply calling the relay
method as implemented above. This is because the following processing is implicitly performed in the relay
method.
- Generate GraphQL object types required for Relay-style
- Set the necessary args (first, last, before, and after) for Relay-style
- Format values retrieved from DB into Relay-style format
- Generate a cursor for each node
- Calculate PageInfo such as hasNextPage and hasPreviousPage
You can also customize the details. By default, tasks are sorted in ascending order by id, but you can change the order using the relayOrderBy
method.
const tasksQuery = pg.query({
name: 'tasks',
field: (b) =>
b
.object(() => objects.Task)
.relay()
+ .relayOrderBy({ updatedAt: 'desc' })
.resolve(({ prismaArgs }) => prisma.task.findMany(prismaArgs))
})
Also, by calling the relay
method, the first
, last
, before
, and after
args are set, but the relayArgs
method can be used to set default values or validations for those.
const tasksQuery = pg.query({
name: 'tasks',
field: (b) =>
b
.object(() => objects.Task)
.relay()
.relayOrderBy({ updatedAt: 'desc' })
+ .relayArgs((f) => ({
+ ...f,
+ first: f.first.default(10).validation((schema) => schema.max(100)),
+ last: last: f.last.validation((schema) => schema.max(100)),
+ }))
.resolve(({ prismaArgs }) => prisma.task.findMany(prismaArgs))
})
In addition, the relayTotalCount
method can be used to add a field to retrieve the TotalCount.
const tasksQuery = pg.query({
name: 'tasks',
field: (b) =>
b
.object(() => objects.Task)
.relay()
.relayOrderBy([{ updatedAt: 'desc' }, { id: 'desc' }])
.relayArgs((f) => ({
...f,
first: f.first.default(10).validation((schema) => schema.max(100)),
last: last: f.last.validation((schema) => schema.max(100)),
}))
+ .relayTotalCount(() => prisma.task.count())
.resolve(({ prismaArgs }) => prisma.task.findMany(prismaArgs))
})
After restarting the server, you should be able to get the totalCount as follows. You will also see that the default values and validations for first
and last
are in effect.
query {
tasks {
totalCount
edges {
node {
id
}
}
}
}
Context setting and Authorization
Up to this point, we have not mentioned any authorization controls. Revisiting the API requirements listed at the beginning of the tutorial, it appears that the following modifications are necessary for queries we have implemented so far.
- Since tasksQuery returns all tasks, it should return only the calling user's own tasks
- Only the administrator user can get user list, so usersQuery should be called only by the administrator user
To implement authorization control, it is essential to understand a concept called Context
in GraphQL. The Context
mainly stores information about the caller of the API. For example, the ID of the user who made the execution, the IP address of the caller, and so on.
We need to make sure that a context is generated with each request, which is the responsibility of the GraphQL Server (GraphQL Yoga or Apollo Server), not PlanetGraphQL (GraphQL Schema Builder). Let's add a process to generate a context for each request in the createServer
method of GraphQL Yoga.
const server = createServer({
schema: pg.build([usersQuery, tasksQuery]),
maskedErrors: false,
+ context: ({ req }) => ({
+ userId: Number(req.headers['x-user-id'] ?? 0),
+ isAdmin: Boolean(req.headers['x-is-admin'] ?? false),
+ })
})
Now the x-user-id
and x-is-admin
values in the request header will be set in the context and will be available in PlanetGraphQL. Since this is a sample, if there is no value in the header, a fixed value will be set.
Next, to make it easier to handle context in PlanetGraphQL, we pass the context type information to PlanetGraphQL. Modify index.ts as follows.
+ type TContext = {
+ userId: number
+ isAdmin: boolean
+ }
- const pg = getPGBuilder<{ Prisma: PrismaTypes }>()
+ const pg = getPGBuilder<{ Context: TContext, Prisma: PrismaTypes }>()
We are now ready for Context. Let's limit the tasks that can be retrieved by tasksQuery to the caller's own tasks. Modify tasksQuery as follows:
const tasksQuery = pg.query({
name: 'tasks',
field: (b) =>
b
.object(() => objects.Task)
.relay()
.relayOrderBy([{ updatedAt: 'desc' }, { id: 'desc' }])
.relayArgs((f) => ({
...f,
first: f.first.default(10).validation((schema) => schema.max(100)),
last: f.last.default(10).validation((schema) => schema.max(100)),
}))
- .relayTotalCount(() => prisma.task.count())
+ .relayTotalCount(({ context }) => prisma.task.count({
+ where: { userId: context.userId },
+ }))
- .resolve(({ prismaArgs }) => prisma.task.findMany(prismaArgs))
+ .resolve(({ context, prismaArgs }) => prisma.task.findMany({
+ ...prismaArgs,
+ where: { userId: context.userId },
+ }))
})
We used userId
in the context to limit the data to be retrieved only to tasks associated with the calling user.
Next, let's modify usersQuery so that it can be called only by the administrator user. Modify usersQuery as follows:
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
+ .auth(({ context }) => context.isAdmin, { strict: true })
.prismaArgs(() => args.findManyUser.edit((f) => ({
})
You can control permissions on a field-by-field basis by returning a boolean of the authorization status in the auth
method. Restart the server and then call usersQuery. An error should be returned unless you add { "x-is-admin": true }
to the request header.
Finally, remove the { strict: true }
to better understand the behavior of the auth
method.
const usersQuery = pg.query({
name: 'users',
field: (b) =>
b
.object(() => objects.User)
.list()
- .auth(({ context }) => context.isAdmin, { strict: true })
+ .auth(({ context }) => context.isAdmin)
.prismaArgs(() => args.findManyUser.edit((f) => ({
})
Restart the server and this time an empty array should be returned instead of an error. The default behavior of the auth
method is to return a response if there are values that can be returned without authorization. Specifically, the behavior is as follows:
- If the return value is List type, return an empty array if there is no authorization
- If the return value is nullable, return null if there is no authorization
- otherwise an error occurs
Mutation implementation and copy
Finally, let's implement mutations. The first is a mutation that creates a user.
Please add the following to index.ts.
const taskEnum = pg.enum({
name: 'TaskEnum',
values: ['new', 'in_progress', 'done']
})
const createTaskInput = pg.input({
name: 'CreateTaskInput',
fields: (b) => ({
title: b.string().validation((schema) => schema.max(100)),
content: b.string().nullable(),
status: b.enum(taskEnum),
dueAt: b.dateTime(),
})
}).validation((value) => value.title.length > 0 || value.status !== 'new')
const createTaskMutation = pg.mutation({
name: 'createTask',
field: (b) =>
b
.object(() => task)
.args((b) => ({
input: b.input(() => createTaskInput)
}))
.resolve(({ context, args }) => prisma.task.create({
data: {
...args.input,
userId: context.userId
}
}))
})
Don't forget to add the newly defined createTaskMutation
to the arguments of the pg.build
method.
const server = createServer({
- schema: pg.build([usersQuery, tasksQuery]),
+ schema: pg.build([usersQuery, tasksQuery, createTaskMutation]),
maskedErrors: false,
})
Here, we use pg.input
to define the Input type of GraphQL without using PGArgBuilder. We also use pg.enum
to define the Enum type of GraphQL so that we can specify the task status with this enum.
Let's call createTask
with the following query. A new task associated with the calling user should be created and returned.
mutation {
createTask(input: {
title: "some title",
content: null,
dueAt: "2030-01-01T12:00:00Z",
status: new
}) {
id
title
content
dueAt
status
}
}
Let's also create a Mutation for updating the task. Add the following to index.ts.
const updateTaskInput = createTaskInput.copy({
name: 'UpdateTaskInput',
fields: (f, b) => ({
...f,
id: b.int(),
})
})
const updateTaskMutation = pg.mutation({
name: 'updateTask',
field: (b) =>
b
.object(() => task)
.args((b) => ({
input: b.input(() => updateTaskInput)
}))
.resolve(async ({ context, args}) => {
await prisma.task.findFirstOrThrow({
where: {
id: args.input.id,
userId: context.userId,
}
})
return prisma.task.update({
where: {
id: args.input.id,
},
data: args.input
})
})
})
Remember to add updateTaskMutation
as an argument to the pg.build
method.
const server = createServer({
- schema: pg.build([usersQuery, tasksQuery, createTaskMutation]),
+ schema: pg.build([usersQuery, tasksQuery, createTaskMutation, updateTaskMutation]),
maskedErrors: false,
})
The copy
method of CreateTaskInput
is used to create an UpdateTaskInput
for updating. Since the update requires the ID of the target task, we add an id
field to receive the ID. Restart the server and make sure that updateTask
can be called.
mutation {
updateTask(input: {
id: 0,
title: "some updated title",
content: null,
dueAt: "2030-01-01T12:00:00Z",
status: new
}) {
id
title
content
dueAt
status
}
}
File structure
So far, all implementations have been written in index.ts, but in actual applications, it is necessary to consider a more manageable file structure. Here we introduce a recommended file structure as PlanetGraphQL.
-
src/models/*.ts
: Define each model -
src/resolvers/*.ts
: Define field resolvers for each model, and Query, Mutation, Subscription associated with each model -
src/builder.ts
: Initialize PlanetGraphQL -
src/server.ts
: Initialize and start the GraphQL server
Please check this repository for the actual source file after the file structure changes.
In conclusion
We hope that you found it simple to implement GraphQL APIs using PlanetGraphQL and Prisma.
PlanetGraphQL is a young project. If you notice anything that could be improved, please submit an issue on Github! Have a nice GraphQL life!
Top comments (0)