PJPalJS

Prisma SDL Generator

Generate GraphQL Schema Definition Language (SDL) files and corresponding resolvers for Prisma models. This generator creates complete GraphQL schemas with type definitions and resolver implementations using a schema-first approach.

View Template on GitHub

Overview

The Prisma SDL Generator creates comprehensive GraphQL schema files using Schema Definition Language (SDL). Unlike the Nexus generator which uses code-first approach, this generator follows the schema-first GraphQL pattern:

  • SDL type definitions for all Prisma models
  • Complete resolver implementations with TypeScript support
  • Input types and enums in SDL format
  • Query and mutation operations for CRUD functionality
  • Type-safe resolver functions with proper signatures

Configuration Options

typescript
interface SDLGeneratorOptions {
  prismaName?: string; // Prisma client instance name (default: "prisma")
  output?: string; // Output directory (default: "./src/graphql")
  javaScript?: boolean; // Generate JS instead of TS (default: false)
  excludeFields?: string[]; // Fields to exclude globally
  excludeModels?: Array<{
    name: string;
    queries?: boolean;
    mutations?: boolean;
  }>;
  excludeFieldsByModel?: Record<string, string[]>;
  excludeQueriesAndMutations?: string[];
  excludeQueriesAndMutationsByModel?: Record<string, string[]>;
  excludeInputFields?: string[];
  disableQueries?: boolean; // Disable all query generation
  disableMutations?: boolean; // Disable all mutation generation
}

Generated File Structure

code
src/graphql/
├── resolvers.ts         # Main resolver exports
├── typeDefs.ts          # Main type definition exports
├── resolversTypes.ts    # Generated TypeScript types
├── InputTypes.ts        # Input types and enums SDL
└── {ModelName}/
    ├── resolvers.ts     # Model-specific resolvers
    └── typeDefs.ts      # Model-specific type definitions

Generation Patterns

1. Type Definition Generation (typeDefs.ts)

graphql
"""
User model representing blog authors and commenters
"""
type User {
  """
  Unique identifier for the user
  """
  id: String!
 
  """
  User's email address (unique)
  """
  email: String!
 
  """
  User's display name
  """
  name: String
 
  """
  Profile image URL
  """
  avatar: String
 
  """
  User's role in the system
  """
  role: Role!
 
  """
  Posts authored by this user
  """
  posts(
    where: PostWhereInput
    orderBy: [PostOrderByWithRelationInput!]
    cursor: PostWhereUniqueInput
    take: Int
    skip: Int
    distinct: [PostScalarFieldEnum!]
  ): [Post!]!
 
  """
  Comments made by this user
  """
  comments(
    where: CommentWhereInput
    orderBy: [CommentOrderByWithRelationInput!]
    cursor: CommentWhereUniqueInput
    take: Int
    skip: Int
    distinct: [CommentScalarFieldEnum!]
  ): [Comment!]!
}
 
enum Role {
  USER
  ADMIN
  MODERATOR
}

2. Query Type Generation

graphql
type Query {
  findUniqueUser(where: UserWhereUniqueInput!): User
 
  findManyUser(
    where: UserWhereInput
    orderBy: [UserOrderByWithRelationInput!]
    cursor: UserWhereUniqueInput
    take: Int
    skip: Int
    distinct: [UserScalarFieldEnum!]
  ): [User!]!
 
  findManyUserCount(
    where: UserWhereInput
    orderBy: [UserOrderByWithRelationInput!]
    cursor: UserWhereUniqueInput
    take: Int
    skip: Int
    distinct: [UserScalarFieldEnum!]
  ): Int!
 
  findFirstUser(
    where: UserWhereInput
    orderBy: [UserOrderByWithRelationInput!]
    cursor: UserWhereUniqueInput
    take: Int
    skip: Int
    distinct: [UserScalarFieldEnum!]
  ): User
 
  aggregateUser(
    where: UserWhereInput
    orderBy: [UserOrderByWithRelationInput!]
    cursor: UserWhereUniqueInput
    take: Int
    skip: Int
  ): AggregateUser
}

3. Mutation Type Generation

graphql
type Mutation {
  createOneUser(data: UserCreateInput!): User!
 
  updateOneUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User!
 
  deleteOneUser(where: UserWhereUniqueInput!): User
 
  upsertOneUser(
    where: UserWhereUniqueInput!
    create: UserCreateInput!
    update: UserUpdateInput!
  ): User!
 
  deleteManyUser(where: UserWhereInput): BatchPayload!
 
  updateManyUser(
    where: UserWhereInput
    data: UserUpdateManyMutationInput!
  ): BatchPayload!
}

4. Resolver Implementation (resolvers.ts)

typescript
import { Resolvers } from "../resolversTypes";
 
const userResolvers: Resolvers = {
  Query: {
    findUniqueUser: (_parent, { where }, { prisma }) => {
      return prisma.user.findUnique({ where });
    },
 
    findManyUser: (_parent, args, { prisma }) => {
      return prisma.user.findMany(args);
    },
 
    findManyUserCount: (_parent, args, { prisma }) => {
      return prisma.user.count(args);
    },
 
    findFirstUser: (_parent, args, { prisma }) => {
      return prisma.user.findFirst(args);
    },
 
    aggregateUser: (_parent, args, { prisma }) => {
      return prisma.user.aggregate(args);
    },
  },
 
  Mutation: {
    createOneUser: (_parent, { data }, { prisma }) => {
      return prisma.user.create({ data });
    },
 
    updateOneUser: (_parent, { where, data }, { prisma }) => {
      return prisma.user.update({ where, data });
    },
 
    deleteOneUser: (_parent, { where }, { prisma }) => {
      return prisma.user.delete({ where });
    },
 
    upsertOneUser: (_parent, { where, create, update }, { prisma }) => {
      return prisma.user.upsert({ where, create, update });
    },
 
    deleteManyUser: (_parent, { where }, { prisma }) => {
      return prisma.user.deleteMany({ where });
    },
 
    updateManyUser: (_parent, { where, data }, { prisma }) => {
      return prisma.user.updateMany({ where, data });
    },
  },
 
  User: {
    posts: (parent, args, { prisma }) => {
      return prisma.user
        .findUnique({
          where: { id: parent.id },
        })
        .posts(args);
    },
 
    comments: (parent, args, { prisma }) => {
      return prisma.user
        .findUnique({
          where: { id: parent.id },
        })
        .comments(args);
    },
  },
};
 
export default userResolvers;

Real-World Example

Sample Prisma Schema

prisma
// schema.prisma
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  avatar    String?
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  posts    Post[]
  comments Comment[]
}
 
model Post {
  id        String   @id @default(uuid())
  title     String
  content   String?
  published Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  authorId String
  author   User      @relation(fields: [authorId], references: [id])
  comments Comment[]
}
 
model Comment {
  id        String   @id @default(uuid())
  content   String
  approved  Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  postId   String
  post     Post   @relation(fields: [postId], references: [id])
  authorId String
  author   User   @relation(fields: [authorId], references: [id])
}
 
enum Role {
  USER
  ADMIN
  MODERATOR
}

AI Prompt

code
I need to generate GraphQL SDL schema and resolvers for my blog application using the PalJS patterns.

Please follow the Prisma SDL Generator template with this configuration:
- Output directory: src/graphql
- Exclude fields: ["createdAt", "updatedAt"]
- Models to generate: ["User", "Post", "Comment"]
- Generate TypeScript (not JavaScript)
- Prisma client name: "prisma"

[Include the complete Prisma SDL Generator template from GitHub]

Here's my Prisma schema:
[Include the schema above]

Generate the SDL files and resolvers following the template patterns.

Generated Schema Setup

src/graphql/typeDefs.ts:

typescript
import { gql } from "apollo-server-express";
import userTypeDefs from "./User/typeDefs";
import postTypeDefs from "./Post/typeDefs";
import commentTypeDefs from "./Comment/typeDefs";
import inputTypes from "./InputTypes";
 
const typeDefs = gql`
  type Query {
    # User queries
    findUniqueUser(where: UserWhereUniqueInput!): User
    findManyUser(
      where: UserWhereInput
      orderBy: [UserOrderByWithRelationInput!]
      cursor: UserWhereUniqueInput
      take: Int
      skip: Int
    ): [User!]!
    findManyUserCount(
      where: UserWhereInput
      orderBy: [UserOrderByWithRelationInput!]
      cursor: UserWhereUniqueInput
      take: Int
      skip: Int
    ): Int!
 
    # Post queries
    findUniquePost(where: PostWhereUniqueInput!): Post
    findManyPost(
      where: PostWhereInput
      orderBy: [PostOrderByWithRelationInput!]
      cursor: PostWhereUniqueInput
      take: Int
      skip: Int
    ): [Post!]!
    findManyPostCount(
      where: PostWhereInput
      orderBy: [PostOrderByWithRelationInput!]
      cursor: PostWhereUniqueInput
      take: Int
      skip: Int
    ): Int!
 
    # Comment queries
    findUniqueComment(where: CommentWhereUniqueInput!): Comment
    findManyComment(
      where: CommentWhereInput
      orderBy: [CommentOrderByWithRelationInput!]
      cursor: CommentWhereUniqueInput
      take: Int
      skip: Int
    ): [Comment!]!
    findManyCommentCount(
      where: CommentWhereInput
      orderBy: [CommentOrderByWithRelationInput!]
      cursor: CommentWhereUniqueInput
      take: Int
      skip: Int
    ): Int!
  }
 
  type Mutation {
    # User mutations
    createOneUser(data: UserCreateInput!): User!
    updateOneUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User!
    deleteOneUser(where: UserWhereUniqueInput!): User
 
    # Post mutations
    createOnePost(data: PostCreateInput!): Post!
    updateOnePost(where: PostWhereUniqueInput!, data: PostUpdateInput!): Post!
    deleteOnePost(where: PostWhereUniqueInput!): Post
 
    # Comment mutations
    createOneComment(data: CommentCreateInput!): Comment!
    updateOneComment(
      where: CommentWhereUniqueInput!
      data: CommentUpdateInput!
    ): Comment!
    deleteOneComment(where: CommentWhereUniqueInput!): Comment
  }
 
  ${userTypeDefs}
  ${postTypeDefs}
  ${commentTypeDefs}
  ${inputTypes}
`;
 
export default typeDefs;

src/graphql/resolvers.ts:

typescript
import userResolvers from "./User/resolvers";
import postResolvers from "./Post/resolvers";
import commentResolvers from "./Comment/resolvers";
import { mergeResolvers } from "@graphql-tools/merge";
 
const resolvers = mergeResolvers([
  userResolvers,
  postResolvers,
  commentResolvers,
]);
 
export default resolvers;

Schema Composition and Server Setup

Apollo Server Setup

typescript
// src/server.ts
import { ApolloServer } from "apollo-server-express";
import { makeExecutableSchema } from "@graphql-tools/schema";
import express from "express";
import typeDefs from "./graphql/typeDefs";
import resolvers from "./graphql/resolvers";
import { createContext } from "./context";
 
const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
});
 
async function startServer() {
  const app = express();
 
  const server = new ApolloServer({
    schema,
    context: createContext,
    introspection: true,
    playground: true,
  });
 
  await server.start();
  server.applyMiddleware({ app, path: "/graphql" });
 
  const port = process.env.PORT || 4000;
 
  app.listen(port, () => {
    console.log(
      `🚀 Server ready at http://localhost:${port}${server.graphqlPath}`
    );
  });
}
 
startServer().catch((error) => {
  console.error("Error starting server:", error);
});

Context Setup

typescript
// src/context.ts
import { PrismaClient } from "@prisma/client";
import { Request } from "express";
 
export interface Context {
  prisma: PrismaClient;
  user?: {
    id: string;
    email: string;
    role: string;
  };
}
 
export const prisma = new PrismaClient();
 
export function createContext(req: Request): Context {
  // Add authentication logic here
  return {
    prisma,
    // user: getUser(req),
  };
}

Advanced Features

Custom Scalar Types

typescript
// src/graphql/scalars.ts
import { GraphQLScalarType } from "graphql";
import { Kind } from "graphql/language";
 
export const DateTimeScalar = new GraphQLScalarType({
  name: "DateTime",
  description: "Date custom scalar type",
  serialize(value: Date) {
    return value.toISOString();
  },
  parseValue(value: string) {
    return new Date(value);
  },
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    return null;
  },
});
 
// Add to resolvers
const resolvers = {
  DateTime: DateTimeScalar,
  // ... other resolvers
};

Field-Level Authorization

typescript
// Enhanced resolvers with authorization
const userResolvers: Resolvers = {
  Query: {
    findManyUser: async (_parent, args, { prisma, user }) => {
      // Admin can see all users, regular users only see themselves
      if (user?.role !== "ADMIN") {
        args.where = { ...args.where, id: user?.id };
      }
      return prisma.user.findMany(args);
    },
  },
 
  User: {
    email: (parent, _args, { user }) => {
      // Only return email if it's the user's own data or admin
      if (user?.id === parent.id || user?.role === "ADMIN") {
        return parent.email;
      }
      return null;
    },
  },
};

Input Validation

typescript
import Joi from "joi";
 
const userCreateSchema = Joi.object({
  email: Joi.string().email().required(),
  name: Joi.string().min(2).max(100),
  role: Joi.string().valid("USER", "ADMIN", "MODERATOR").default("USER"),
});
 
const userResolvers: Resolvers = {
  Mutation: {
    createOneUser: async (_parent, { data }, { prisma }) => {
      // Validate input
      const { error, value } = userCreateSchema.validate(data);
      if (error) {
        throw new Error(`Validation error: ${error.details[0].message}`);
      }
 
      return prisma.user.create({ data: value });
    },
  },
};

Type Safety Benefits

Generated Types for Resolvers

The SDL generator creates comprehensive TypeScript types that ensure:

typescript
// Generated resolversTypes.ts provides full type safety
export type QueryResolvers = {
  findUniqueUser?: Resolver<{}, QueryFindUniqueUserArgs, User | null>;
  findManyUser?: Resolver<{}, QueryFindManyUserArgs, User[]>;
  findManyUserCount?: Resolver<{}, QueryFindManyUserCountArgs, number>;
};
 
export type UserResolvers = {
  posts?: Resolver<User, UserPostsArgs, Post[]>;
  comments?: Resolver<User, UserCommentsArgs, Comment[]>;
};
 
// Type-safe resolver implementation
const resolvers: Resolvers = {
  Query: {
    findUniqueUser: (parent, args, context) => {
      // TypeScript knows the exact types of parent, args, and context
      return context.prisma.user.findUnique({ where: args.where });
    },
  },
};

Testing

Unit Testing Resolvers

typescript
// tests/resolvers.test.ts
import { createTestContext } from "./__helpers";
 
const ctx = createTestContext();
 
describe("User resolvers", () => {
  test("findUniqueUser returns user by ID", async () => {
    const user = await ctx.prisma.user.create({
      data: {
        email: "test@example.com",
        name: "Test User",
      },
    });
 
    const result = await ctx.resolvers.Query.findUniqueUser(
      {},
      { where: { id: user.id } },
      ctx.context,
      {} as any
    );
 
    expect(result).toMatchObject({
      id: user.id,
      email: "test@example.com",
      name: "Test User",
    });
  });
});

Integration Testing with GraphQL

typescript
// tests/integration.test.ts
import { createTestClient } from "apollo-server-testing";
import { gql } from "apollo-server-express";
 
const GET_USERS = gql`
  query GetUsers($take: Int) {
    findManyUser(take: $take) {
      id
      email
      name
      role
    }
  }
`;
 
test("can query users", async () => {
  const { query } = createTestClient(server);
 
  const result = await query({
    query: GET_USERS,
    variables: { take: 10 },
  });
 
  expect(result.errors).toBeUndefined();
  expect(result.data.findManyUser).toHaveLength(10);
});

Performance Optimization

DataLoader Integration

typescript
import DataLoader from "dataloader";
 
export function createLoaders(prisma: PrismaClient) {
  return {
    userLoader: new DataLoader(async (userIds: string[]) => {
      const users = await prisma.user.findMany({
        where: { id: { in: userIds } },
      });
      return userIds.map((id) => users.find((user) => user.id === id));
    }),
 
    postsByAuthorLoader: new DataLoader(async (authorIds: string[]) => {
      const posts = await prisma.post.findMany({
        where: { authorId: { in: authorIds } },
      });
      return authorIds.map((authorId) =>
        posts.filter((post) => post.authorId === authorId)
      );
    }),
  };
}
 
// Use in resolvers
const userResolvers: Resolvers = {
  User: {
    posts: (parent, args, { loaders }) => {
      return loaders.postsByAuthorLoader.load(parent.id);
    },
  },
};

Best Practices

Schema Organization

  • Modular approach - Separate type definitions and resolvers by domain
  • Consistent naming - Follow GraphQL conventions for operations
  • Documentation - Include descriptions for all types and fields
  • Input validation - Always validate user inputs
  • Error handling - Provide meaningful error messages

Performance

  • Use DataLoaders - Prevent N+1 query problems
  • Implement pagination - Handle large datasets properly
  • Field selection - Only fetch requested data
  • Query complexity analysis - Limit expensive operations

Security

  • Authentication - Verify user identity
  • Authorization - Check permissions in resolvers
  • Input sanitization - Validate and sanitize all inputs
  • Rate limiting - Prevent abuse

Next Steps

Command Palette

Search for a command to run...