Skip to content
GQLoom

Prisma

Prisma ORM offers developers a brand - new experience when working with databases, thanks to its intuitive data models, automatic migrations, type safety, and auto - completion features.

@gqloom/prisma provides the integration of GQLoom and Prisma:

  • Generate silk from Prisma Schema.
  • Use the resolver factory to quickly create CRUD operations from Prisma.

Installation

Please refer to Prisma's documentation to install Prisma and the corresponding database driver.

sh
npm i graphql @gqloom/core @gqloom/prisma
sh
pnpm add graphql @gqloom/core @gqloom/prisma
sh
yarn add graphql @gqloom/core @gqloom/prisma
sh
bun add graphql @gqloom/core @gqloom/prisma
sh
deno add npm:graphql npm:prisma npm:@gqloom/core npm:@gqloom/prisma

Configuration

Define your Prisma Schema in the prisma/schema.prisma file:

prisma
generator client {
  provider = "prisma-client-js"
}

generator gqloom { 
  provider = "prisma-gqloom"
} 

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Generator Parameters

The generator accepts the following parameters:

ParameterDescriptionDefault Value
gqloomPathThe path to the GQLoom package.@gqloom/prisma
clientOutputThe path to the Prisma client.node_modules/@prisma/client
outputThe folder path where the generated files will be located.node_modules/@gqloom/prisma/generated
commonjsFileThe file name of the CommonJS file. Use an empty string "" to skip generation of the CommonJS file.index.cjs
moduleFileThe file name of the ES module file. Use an empty string "" to skip generation of the ES module file.index.js
typesFilesThe file name(s) of the TypeScript declaration file(s). Use [] to skip generation of the TypeScript declaration file(s).["index.d.ts"]

Generate Silk

sh
npx prisma generate

Using Silk

After generating the silk, we can use it in the resolver. We use useSelectedFields to ensure only the fields required by the GraphQL query are selected:

ts
import { resolver, query, field, weave } from '@gqloom/core'
import { asyncContextProvider } from '@gqloom/core/context'
import { useSelectedFields } from "@gqloom/prisma/context"
import { ValibotWeaver } from '@gqloom/valibot'
import { Post, User } from '@gqloom/prisma/generated'
import * as v from 'valibot'
import { PrismaClient } from '@prisma/client'

const db = new PrismaClient({})

const userResolver = resolver.of(User, {
  user: query(User.nullable(), {
    input: { id: v.number() },
    resolve: ({ id }) => {
      return db.user.findUnique({
        select: useSelectedFields(User),
        where: { id },
      })
    },
  }),

  posts: field(Post.list(), async (user) => {
    const posts = await db.user
      .findUnique({ where: { id: user.id } })
      .posts({ select: useSelectedFields(Post) })
    return posts ?? []
  }),
})

const postResolver = resolver.of(Post, {
  author: field(User.nullable())
    .derivedFrom("authorId")
    .resolve((post) => {
      if (!post.authorId) return null
      return db.user.findUnique({ where: { id: post.authorId } })
    }),
})

export const schema = weave(asyncContextProvider, ValibotWeaver, userResolver, postResolver)

As shown in the code above, we can directly use the types generated by Prisma within the resolver. Here, we've defined two resolvers: userResolver and postResolver.

In userResolver, we use User as the parent type for resolver.of and define two fields:

  • The user query: The return type is User.nullable(), indicating it may return a single user or null. It accepts an id parameter and uses Prisma's findUnique method to query the database.
  • The posts field: The return type is Post.list(), meaning it returns a list of all articles by the user. It fetches the user's articles through Prisma's relational queries.

In postResolver, we use Post as the parent type and define one field:

  • The author field: The return type is User, representing the author of the article. It retrieves the author information through Prisma's relational queries.

All queries utilize the useSelectedFields() function to ensure that only the fields requested in the GraphQL query are selected. This helps optimize database query performance. This function requires enabling context. For runtimes where the useSelectedFields() function cannot be used, we can also use the getSelectedFields() function to obtain the columns that need to be selected for the current query.

Derived Fields

Adding derived fields to the model is quite simple. However, it's important to use the field().derivedFrom() method to declare the columns on which it depends, so that the useSelectedFields method can correctly select these columns:

ts
export const postResolver = resolver.of(Post, {
  abstract: field(v.string())
    .derivedFrom("title", "content")
    .resolve((post) => {
      return `${post.title} ${post.content?.slice(0, 60)}...`
    }),
})

Hiding Fields

@gqloom/prisma exposes all fields by default. If you want to hide certain fields, you can use field.hidden:

ts
const postResolver = resolver.of(Post, {
  author: field(User, async (post) => {
    const author = await db.post.findUnique({ where: { id: post.id } }).author()
    return author!
  }),

  authorId: field.hidden, 
})

In the above code, we hide the authorId field, which means it will not appear in the generated GraphQL Schema.

Model Configuration

You can customize output fields, input behavior, and metadata for a specific Prisma model via the .config() method on the generated silk.

Output Field Configuration

You can use the fields option to customize the GraphQL Object Type generated from the model. This lets you override field types, add descriptions, or hide specific fields.

ts
import { User } from '@gqloom/prisma/generated'
import { weave, SYMBOLS } from '@gqloom/core'
import { GraphQLID } from 'graphql'

const userConfig = User.config({
  description: "System user information", // Add description to the GraphQL type
  fields: {
    // Override field description
    email: { description: "User's unique email address" },
    // Override field type; supports GraphQL type or silk
    id: { type: GraphQLID },
    // Hide field so it does not appear in query results
    password: SYMBOLS.FIELD_HIDDEN,
  },
})

export const schema = weave(userConfig, userResolver, postResolver)

Input Field Behavior

You can use the input option to control how fields behave in various input types (e.g. CreateInput, UpdateInput, WhereInput). You can decide per "operation" whether a field is visible or override its input type.

Supported operation types:

  • create: Input for create operations (e.g. UserCreateInput).
  • update: Input for update operations (e.g. UserUpdateInput).
  • filters: Input for filter operations (e.g. UserWhereInput).
ts
import { User } from '@gqloom/prisma/generated'
import { weave } from '@gqloom/core'
import * as v from 'valibot'

const userConfig = User.config({
  input: {
    // Hide email in create
    email: { create: false },
    // Override name in update with a required string (via silk)
    name: { update: v.string() },
    // By default hide filter for all fields
    "*": { filters: false },
    // Only enable filter for id
    id: { filters: true },
  },
})

export const schema = weave(userConfig, userResolver, postResolver)

Priority

For input types, behavior defined in the input option has the highest priority; it overrides both fields config and global presets.

Resolver Factory

@gqloom/prisma provides the PrismaResolverFactory to help you create resolver factories. With the resolver factory, you can quickly define common queries, mutations, and fields. The resolver factory also pre-defines input types for common operations. Using it can greatly reduce boilerplate, which is helpful for fast iteration.

ts
import { Post, User } from '@gqloom/prisma/generated'
import { PrismaResolverFactory } from '@gqloom/prisma'

import { PrismaClient } from '@prisma/client'

const db = new PrismaClient({})

const userResolverFactory = new PrismaResolverFactory(User, db)
const postResolverFactory = new PrismaResolverFactory(Post, db)

In the above code, we create resolver factories for the User and Post models. PrismaResolverFactory accepts two arguments: the first is the model used as silk, the second is a PrismaClient instance.

Relationship Fields

The resolver factory provides the relationField method to define relationship fields:

ts
const userResolver = resolver.of(User, {
  user: query(User.nullable(), {
    input: { id: v.number() },
    resolve: ({ id }) => {
      return db.user.findUnique({ where: { id } })
    },
  }),

  posts: field(Post.list(), async (user) => { 
    const posts = await db.user.findUnique({ where: { id: user.id } }).posts() 
    return posts ?? [] 
  }), 
  posts: userResolverFactory.relationField('posts'), 
})

const postResolver = resolver.of(Post, {
  author: field(User, async (post) => { 
    const author = await db.post.findUnique({ where: { id: post.id } }).author() 
    return author!
  }), 
  author: postResolverFactory.relationField('author'), 

  authorId: field.hidden,
})

In the above code, we use userResolverFactory.relationField('posts') and postResolverFactory.relationField('author') to define relationship fields. The relationField method accepts a string argument: the name of the relationship field.

Queries

The resolver factory pre-defines common queries:

  • countQuery
  • findFirstQuery
  • findManyQuery
  • findUniqueQuery

You can use them directly:

ts
const userResolver = resolver.of(User, {
  user: query(User.nullable(), { 
    input: { id: v.number() }, 
    resolve: ({ id }) => { 
      return db.user.findUnique({ where: { id } }) 
    }, 
  }), 
  user: userResolverFactory.findUniqueQuery(), 

  posts: userResolverFactory.relationField('posts'),
})

In the above code, we use userResolverFactory.findUniqueQuery() to define the user query. The resolver factory creates the input type and resolver function automatically.

Mutations

The resolver factory pre-defines common mutations:

  • createMutation
  • createManyMutation
  • deleteMutation
  • deleteManyMutation
  • updateMutation
  • updateManyMutation
  • upsertMutation

You can use them directly:

ts
const postResolver = resolver.of(Post, {
  createPost: postResolverFactory.createMutation(), 

  author: postResolverFactory.relationField('author'),

  authorId: field.hidden,
})

In the above code, we use postResolverFactory.createMutation() to define the createPost mutation. The factory creates the input type and resolver function automatically.

Custom Input

The resolver factory’s pre-defined queries and mutations support custom input. You can define the input type via the input option:

ts
import * as v from "valibot"

const userResolver = resolver.of(User, {
  user: userResolverFactory.findUniqueQuery().input(
    v.pipe( 
      v.object({ id: v.number() }), 
      v.transform(({ id }) => ({ where: { id } })) 
    ) 
  ),

  posts: userResolverFactory.relationField("posts"),
})

In the above code, we use valibot to define the input type. v.object({ id: v.number() }) defines the input object type, and v.transform(({ id }) => ({ where: { id } })) maps the input to Prisma query arguments.

Adding Middleware

The resolver factory’s pre-defined queries, mutations, and fields support middleware. You can define it via the middlewares option:

ts
const postResolver = resolver.of(Post, {
  createPost: postResolverFactory.createMutation().use(async (next) => {
    const user = await useAuthedUser() 
    if (user == null) throw new GraphQLError("Please login first") 
    return next() 
  }), 

  author: postResolverFactory.relationField("author"),

  authorId: field.hidden,
})

In the above code, we use the middlewares option to define middleware. async (next) => { ... } is the middleware. useAuthedUser() is a custom function that returns the current user; if not logged in, it throws, otherwise we call next() to continue.

Complete Resolver

You can create a full resolver directly from the resolver factory:

ts
// Readonly Resolver
const userQueriesResolver = userResolverFactory.queriesResolver()

// Full Resolver
const userResolver = userResolverFactory.resolver()

There are two methods:

  • usersResolverFactory.queriesResolver(): Creates a resolver with only queries and relation fields.
  • usersResolverFactory.resolver(): Creates a resolver with all queries, mutations, and relation fields.

Custom Type Mapping

To adapt to more Prisma types, we can extend GQLoom to add more type mappings.

First, use PrismaWeaver.config to define type mapping. Here we import GraphQLDateTime and GraphQLJSON from graphql-scalars. When the types are DateTime or Json, we map them to the corresponding GraphQL scalars.

ts
import { 
GraphQLDateTime
,
GraphQLJSON
} from 'graphql-scalars'
import {
PrismaWeaver
} from '@gqloom/prisma'
export const
prismaWeaverConfig
=
PrismaWeaver
.
config
({
/** * Emit @id fields as GraphQL ID type (output types only). * Default is true. Set to false to use the underlying scalar (e.g. Int or String). */
emitIdAsIDType
: false,
presetGraphQLType
: (
type
) => {
switch (
type
) {
case 'DateTime': return
GraphQLDateTime
case 'Json': return
GraphQLJSON
} }, })

Pass this config into the weave function when building the GraphQL schema:

ts
import { weave } from "@gqloom/core"

export const schema = weave(prismaWeaverConfig, userResolver, postResolver)

Default Type Mapping

The following table lists the default mapping between Prisma types and GraphQL types in GQLoom:

Prisma TypeGraphQL Type
Int @idGraphQLID
String @idGraphQLID
BigIntGraphQLInt
IntGraphQLInt
DecimalGraphQLFloat
FloatGraphQLFloat
BooleanGraphQLBoolean
DateTimeGraphQLString
StringGraphQLString