npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@eleven-am/nestjs-graphql-crud

v0.0.44

Published

nestjs-graphql-crud is a library that aims to reduce the boilerplate code needed to create a GraphQL CRUD API.

Readme

@eleven-am/nestjs-graphql-crud

npm version

A powerful CRUD module generator for NestJS GraphQL applications. This library helps you rapidly build GraphQL APIs with minimal boilerplate code by automatically generating resolvers, services, and subscriptions.

🌟 Features

  • Dynamic CRUD Module Generation: Automatically creates NestJS modules, services, and GraphQL resolvers based on simple configuration
  • Type Inference: Leverages TypeScript's type system with automatic type inference - no need to specify explicit type parameters
  • Authorization Support: Seamless integration with @eleven-am/authorizer for fine-grained access control across all operations
  • Relation Handling: Easy configuration for one-to-one, one-to-many, and custom relationship resolvers
  • Real-time Updates: Built-in GraphQL subscription support with customizable filtering
  • Database Agnostic: Abstract data layer with ready-to-use Prisma integration, extensible to other ORMs
  • Field Selection: Intelligent selection of requested fields for optimized database queries
  • Advanced Query Support: Full support for Prisma-style features including cursor pagination, ordering, and distinct queries
  • Tool Integration: Compatible with code generators like prisma-nest-graphql

📦 Installation

# Using npm
npm install @eleven-am/nestjs-graphql-crud @nestjs/graphql @nestjs/apollo @apollo/server graphql graphql-subscriptions @nestjs/common @nestjs/core reflect-metadata rxjs @eleven-am/authorizer

# Using yarn
yarn add @eleven-am/nestjs-graphql-crud @nestjs/graphql @nestjs/apollo @apollo/server graphql graphql-subscriptions @nestjs/common @nestjs/core reflect-metadata rxjs @eleven-am/authorizer

# If using Prisma (recommended)
npm install @prisma/client @paljs/plugins
# or
yarn add @prisma/client @paljs/plugins

Note: Ensure you have set up @nestjs/graphql within your project according to the official NestJS documentation.

🚀 Quick Start

Let's set up CRUD operations for User and Post entities using Prisma:

1. Define your entities and input types

Make sure you have your GraphQL entity classes (User, Post) and corresponding input types defined.

2. Configure the CRUD modules in your AppModule:

// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { CrudModulesFactory, PrismaDataProvider, PrismaFieldSelectionProvider } from '@eleven-am/nestjs-graphql-crud';
import { PrismaClient } from '@prisma/client';

// Import your entity classes and GraphQL input types
import { User } from './entities/user.entity';
import { Post } from './entities/post.entity';
import { UserCreateInput, UserUpdateInput, UserUpdateManyInput, UserWhereInput } from './inputs/user';
import { PostCreateInput, PostUpdateInput, PostUpdateManyInput, PostWhereInput } from './inputs/post';

@Module({
  imports: [
    // NestJS GraphQL Module Setup
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
      subscriptions: {
        'graphql-ws': true,
      },
    }),

    // nestjs-graphql-crud Setup
    CrudModulesFactory
      .using(PrismaDataProvider(PrismaClient), PrismaFieldSelectionProvider)
      .forRoot([
        // User CRUD module with posts relation
        CrudModulesFactory.forEntity(User)
          .withConfig({
            modelName: 'user',
            createInput: UserCreateInput,
            updateInput: UserUpdateInput,
            updateManyInput: UserUpdateManyInput,
            whereInput: UserWhereInput,
          })
          .addRelation({
            fieldName: 'posts',
            targetModel: 'post',
            targetType: Post,
            targetWhereInput: PostWhereInput,
            whereNullable: true,
            relationField: 'authorId'
          }),

        // Post CRUD module with author relation
        CrudModulesFactory.forEntity(Post)
          .withConfig({
            modelName: 'post',
            createInput: PostCreateInput,
            updateInput: PostUpdateInput,
            updateManyInput: PostUpdateManyInput,
            whereInput: PostWhereInput,
          })
          .addOneToOneRelation({
            fieldName: 'author',
            targetModel: 'user',
            targetType: User,
            relationField: 'authorId'
          }),
      ]),
  ],
  providers: [PrismaClient],
})
export class AppModule {}

3. Run your NestJS application

The library will generate all necessary resolvers. You can immediately use your GraphQL API with the following operations.

📝 Generated GraphQL Operations

For each configured entity, the library generates:

Queries

  • {modelName}FindOne(where: {WhereInput}): {Entity}
  • {modelName}FindMany(filter: {ModelName}FindManyArgs): [{Entity}]

Mutations

  • {modelName}Create(data: {CreateInput}): {Entity}
  • {modelName}Update(data: {UpdateInput}, id: String): {Entity}
  • {modelName}UpdateMany(data: {UpdateManyInput}, where: {WhereInput}): [{Entity}]
  • {modelName}Delete(id: String): {Entity}
  • {modelName}DeleteMany(where: {WhereInput}): [{Entity}]

Subscriptions

  • {modelName}s(filter: SubscriptionFilter): [{Entity}]

🔧 Advanced Query Features

Using Custom FindMany Args (Relay Pagination & Advanced Filtering)

If you're using code generators like prisma-nest-graphql that produce advanced FindMany argument types with cursor-based pagination, ordering, and distinct queries, you can use them directly:

// Generated by prisma-nest-graphql or similar tools
@ArgsType()
export class FindManyUserArgs {
  @Field(() => UserWhereInput, {nullable: true})
  where?: UserWhereInput;

  @Field(() => [UserOrderByWithRelationInput], {nullable: true})
  orderBy?: Array<UserOrderByWithRelationInput>;

  @Field(() => UserWhereUniqueInput, {nullable: true})
  cursor?: UserWhereUniqueInput;

  @Field(() => Int, {nullable: true})
  take?: number;

  @Field(() => Int, {nullable: true})
  skip?: number;

  @Field(() => [UserScalarFieldEnum], {nullable: true})
  distinct?: Array<UserScalarFieldEnum>;
}

// Use in your CRUD configuration
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })
  .withFindManyArgs(FindManyUserArgs) // 🎉 Enable advanced query features
  .addRelation({
    fieldName: 'posts',
    targetModel: 'post',
    targetType: Post,
    targetWhereInput: PostWhereInput,
    whereNullable: true,
    relationField: 'authorId'
  })

This enables your GraphQL API to support advanced queries like:

query {
  userFindMany(
    where: { name: { contains: "john" } }
    orderBy: [{ createdAt: desc }, { name: asc }]
    cursor: { id: "user123" }
    take: 10
    distinct: [email]
  ) {
    id
    name
    email
    posts {
      title
    }
  }
}

Default vs Custom FindMany Behavior

Without withFindManyArgs() (default behavior):

# Simple take/skip pagination
query {
  userFindMany(filter: {
    where: { name: { contains: "john" } }
    pagination: { take: 10, skip: 0 }
  }) {
    id
    name
  }
}

With withFindManyArgs() (advanced features):

# Full Prisma-style querying
query {
  userFindMany(
    where: { name: { contains: "john" } }
    orderBy: [{ createdAt: desc }]
    cursor: { id: "user123" }
    take: 10
    distinct: [email]
  ) {
    id
    name
  }
}

The library automatically detects which format you're using and handles both seamlessly while maintaining backward compatibility.

🛠 Advanced Usage

Custom Resolvers

Add custom business logic resolvers alongside the generated CRUD operations:

@Injectable()
class UserBusinessLogic {
  constructor(private prisma: PrismaClient) {}

  async findUserByEmail(
    args: { email: string }, 
    ability: AppAbilityType, 
    select: any
  ): Promise<User | null> {
    return this.prisma.user.findUnique({
      where: { email: args.email },
      ...select
    });
  }

  async promoteToAdmin(
    args: { userId: string }, 
    ability: AppAbilityType, 
    select: any
  ): Promise<User> {
    return this.prisma.user.update({
      where: { id: args.userId },
      data: { role: 'ADMIN' },
      ...select
    });
  }

  async getUserStats(
    args: any,
    ability: AppAbilityType, 
    item: User, 
    select: any
  ): Promise<UserStats> {
    // Calculate stats for a specific user
    const postCount = await this.prisma.post.count({
      where: { authorId: item.id }
    });
    
    return { postCount, joinedAt: item.createdAt };
  }
}

// Then in your module configuration:
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })
  .withCustomResolver(UserBusinessLogic)
    .addQuery({
      name: 'findUserByEmail',
      methodName: 'findUserByEmail',
      inputType: FindUserByEmailInput,
      outputType: () => User,
      nullable: true,
      permissions: [{ action: Action.Read, resource: 'User' }]
    })
    .addMutation({
      name: 'promoteUserToAdmin',
      methodName: 'promoteToAdmin',
      inputType: PromoteUserInput,
      outputType: () => User,
      permissions: [{ action: Action.Update, resource: 'User' }]
    })
    .addResolveField({
      name: 'getUserStats',
      methodName: 'getUserStats',
      resolveField: 'stats',
      outputType: () => UserStats,
      permissions: [{ action: Action.Read, resource: 'User' }]
    })
    .and() // Return to main config
  .addRelation({
    fieldName: 'posts',
    targetModel: 'post',
    targetType: Post,
    relationField: 'authorId'
  })

This generates additional GraphQL operations:

type Query {
  # Generated CRUD operations
  userFindOne(where: UserWhereInput): User
  userFindMany(filter: UserFindManyArgs): [User!]!
  
  # Custom query
  findUserByEmail(args: FindUserByEmailInput): User
}

type Mutation {
  # Generated CRUD operations
  userCreate(data: UserCreateInput): User!
  userUpdate(data: UserUpdateInput, id: String!): User!
  # ... other generated mutations
  
  # Custom mutation
  promoteUserToAdmin(args: PromoteUserInput): User!
}

type User {
  id: String!
  email: String!
  name: String!
  
  # Custom field resolver
  stats: UserStats!
  
  # Generated relation
  posts: [Post!]!
}

Custom Relation Resolvers

The library provides powerful ways to handle complex relations beyond simple one-to-many or one-to-one relationships:

@Injectable()
class UserFriendsResolver implements CustomRelationResolver<User, User[], UserWhereInput> {
  constructor(
    @Inject('PrismaService') private prisma: PrismaClient,
  ) {}
  
  async resolve(
    ability: AppAbilityType, 
    user: User, 
    context: FieldSelectionResult<User>,
    args?: FindManyContract<UserWhereInput>
  ): Promise<User[]> {
    // Get IDs of friends from join table
    const friendIds = await this.prisma.friendship.findMany({
      where: { OR: [
        { user1Id: user.id },
        { user2Id: user.id }
      ]},
      select: { 
        user1Id: true, 
        user2Id: true 
      }
    });
    
    // Convert to array of friend IDs that aren't the current user
    const ids = friendIds.map(f => 
      f.user1Id === user.id ? f.user2Id : f.user1Id
    );
    
    // Apply additional filters from args
    const where = {
      id: { in: ids },
      ...(args?.where || {})
    };
    
    // Apply authorization filters from CASL
    return this.prisma.user.findMany({
      where: {
        AND: [
          accessibleBy(ability, Action.Read).user,
          where
        ]
      },
      ...context // Apply field selection
    });
  }
}

// Then in your module configuration:
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    // ... other config
  })
  .addCustomArrayRelation({
    fieldName: 'friends',
    targetModel: 'user',
    targetType: User,
    targetWhereInput: UserWhereInput,
    whereNullable: true,
    factoryClass: UserFriendsResolver
  })

Custom Subscription Filters

Create custom subscription filters to allow clients to receive only relevant updates:

@InputType()
class PostSubscriptionFilter {
  @Field(() => [String], { nullable: true })
  inCategories?: string[];
  
  @Field(() => String, { nullable: true })
  authorId?: string;
  
  @Field(() => Boolean, { nullable: true })
  onlyPublished?: boolean;
}

@Injectable()
class PostSubscriptionResolver implements SubscriptionResolver<Post, PostSubscriptionFilter> {
  constructor(
    @Inject('PrismaService') private prisma: PrismaClient,
  ) {}

  // Determine if a client should receive the update
  filter(filter: PostSubscriptionFilter, changes: Post[]): boolean {
    if (!filter) return true;
    
    return changes.some(post => {
      // Filter by categories if specified
      if (filter.inCategories?.length && 
          !filter.inCategories.includes(post.categoryId)) {
        return false;
      }
      
      // Filter by author if specified
      if (filter.authorId && post.authorId !== filter.authorId) {
        return false;
      }
      
      // Filter by publication status if specified
      if (filter.onlyPublished && !post.published) {
        return false;
      }
      
      return true;
    });
  }
  
  // Optionally enhance or transform the data before sending
  async resolve(filter: PostSubscriptionFilter, changes: Post[]): Promise<Post[]> {
    // You could load additional data or transform the posts
    // For example, add view counts from Redis or compute derived fields
    
    return Promise.all(changes.map(async post => {
      const viewCount = await this.getViewCount(post.id);
      return { ...post, viewCount };
    }));
  }
  
  private async getViewCount(postId: string): Promise<number> {
    // Implementation to get view count from cache/database
    return 0; // Placeholder
  }
}

// Then in your module configuration:
CrudModulesFactory.forEntity(Post)
  .withConfig({
    modelName: 'post',
    // ... other config
  })
  .withSubscription({
    filter: PostSubscriptionFilter,
    resolver: PostSubscriptionResolver
  })

Custom Data Provider

Create a custom data provider for non-Prisma databases or special requirements:

@Injectable()
export class TypeOrmDataProvider implements DataProvider {
  constructor(
    @InjectEntityManager() private entityManager: EntityManager,
    @InjectRepository(User) private userRepo: Repository<User>,
    @InjectRepository(Post) private postRepo: Repository<Post>,
    // ... other repositories
  ) {}

  private getRepository(modelName: string): Repository<any> {
    switch (modelName) {
      case 'user': return this.userRepo;
      case 'post': return this.postRepo;
      // ... other models
      default: throw new Error(`Unknown model: ${modelName}`);
    }
  }

  async findOne<EntityType, WhereInputType>(
    modelName: string,
    ability: AppAbilityType,
    where: WhereInputType,
    select: Record<string, boolean>
  ): Promise<EntityType | null> {
    const repo = this.getRepository(modelName);
    const caslRules = this.translateCaslToTypeOrmConditions(ability, Action.Read, modelName);
    
    return repo.findOne({
      where: {
        ...where,
        ...caslRules
      },
      select: this.translateSelectToTypeOrm(select)
    });
  }

  async findMany<EntityType, WhereInputType>(
    modelName: string,
    ability: AppAbilityType,
    args: {
      where: WhereInputType;
      pagination?: { take?: number; skip?: number };
      orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>;
      cursor?: any;
      distinct?: string[];
    },
    select: Record<string, boolean>
  ): Promise<EntityType[]> {
    const repo = this.getRepository(modelName);
    const caslRules = this.translateCaslToTypeOrmConditions(ability, Action.Read, modelName);
    
    const queryBuilder = repo.createQueryBuilder(modelName);
    
    // Apply where conditions
    queryBuilder.where({
      ...args.where,
      ...caslRules
    });
    
    // Apply ordering
    if (args.orderBy) {
      const orderBy = Array.isArray(args.orderBy) ? args.orderBy[0] : args.orderBy;
      Object.entries(orderBy).forEach(([field, direction]) => {
        queryBuilder.addOrderBy(`${modelName}.${field}`, direction.toUpperCase() as 'ASC' | 'DESC');
      });
    }
    
    // Apply pagination
    if (args.pagination?.take) {
      queryBuilder.limit(args.pagination.take);
    }
    if (args.pagination?.skip) {
      queryBuilder.offset(args.pagination.skip);
    }
    
    // Apply field selection
    queryBuilder.select(this.translateSelectToTypeOrm(select));
    
    return queryBuilder.getMany();
  }

  // Implement other methods similarly...

  private translateCaslToTypeOrmConditions(ability: AppAbilityType, action: Action, modelName: string): any {
    // Translation logic from CASL rules to TypeORM where conditions
    // This would depend on your authorization setup
    return {};
  }

  private translateSelectToTypeOrm(select: Record<string, boolean>): any {
    // Convert GraphQL selection to TypeORM select object
    return Object.keys(select).filter(key => select[key]);
  }
}

// Then use your custom provider:
CrudModulesFactory
  .using(TypeOrmDataProvider, YourFieldSelectionProvider)
  .forRoot([
    // Your module configurations
  ])

Authorization with Custom Policies

Implement fine-grained access control with custom authorization policies:

@Injectable()
export class UserPolicies implements WillAuthorize {
  defineRules(ability: AbilityBuilder<AppAbilityType>, user: UserEntity) {
    if (user.role === 'ADMIN') {
      // Admins can do everything with users
      ability.can(Action.Manage, 'user');
      return;
    }

    // Regular users can read all users
    ability.can(Action.Read, 'user');
    
    // Users can only update and delete their own account
    ability.can([Action.Update, Action.Delete], 'user', { id: user.id });
    
    // Premium users can create new users (invite system)
    if (user.subscription === 'PREMIUM') {
      ability.can(Action.Create, 'user');
    }
  }
}

// Then in your module configuration:
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })
  .withAuthorization(UserPolicies) // Link your custom authorizer

Field Selection Provider for MongoDB

Example of a custom field selection provider for MongoDB:

@Injectable()
export class MongoFieldSelectionProvider implements FieldSelectionProvider {
  parseSelection<EntityType>(info: GraphQLResolveInfo): FieldSelectionResult<EntityType> {
    // Parse GraphQL info into MongoDB projection
    const projection = {};
    const fieldNodes = info.fieldNodes[0]?.selectionSet?.selections || [];
    
    for (const field of fieldNodes) {
      if (field.kind === 'Field') {
        projection[field.name.value] = 1;
        
        // Handle nested selections recursively
        if (field.selectionSet) {
          // Process nested fields for MongoDB subdocuments or lookups
          // ...
        }
      }
    }
    
    return { select: projection as any };
  }
}

// Then use your custom provider:
CrudModulesFactory
  .using(YourMongoDataProvider, MongoFieldSelectionProvider)
  .forRoot([
    // Your module configurations
  ])

Complex Module Setup with Multiple Relations

Example of a complex entity with multiple relation types and advanced features:

import { FindManyBlogPostArgs } from './generated/find-many-blog-post.args'; // Generated by prisma-nest-graphql

CrudModulesFactory.forEntity(BlogPost)
  .withConfig({
    modelName: 'blogPost',
    createInput: BlogPostCreateInput,
    updateInput: BlogPostUpdateInput,
    updateManyInput: BlogPostUpdateManyInput,
    whereInput: BlogPostWhereInput,
  })
  // Enable advanced query features (cursor pagination, orderBy, distinct)
  .withFindManyArgs(FindManyBlogPostArgs)
  // One-to-one relation to author
  .addOneToOneRelation({
    fieldName: 'author',
    targetModel: 'user',
    targetType: User,
    relationField: 'authorId'
  })
  // One-to-many relation to comments
  .addRelation({
    fieldName: 'comments',
    targetModel: 'comment',
    targetType: Comment,
    targetWhereInput: CommentWhereInput,
    whereNullable: true,
    relationField: 'postId'
  })
  // Custom relation to related posts (based on tags)
  .addCustomArrayRelation({
    fieldName: 'relatedPosts',
    targetModel: 'blogPost',
    targetType: BlogPost,
    targetWhereInput: BlogPostWhereInput,
    whereNullable: true,
    factoryClass: BlogPostRelatedResolver
  })
  // Add custom business logic
  .withCustomResolver(BlogPostBusinessLogic)
    .addQuery({
      name: 'findTrendingPosts',
      methodName: 'findTrending',
      inputType: TrendingPostsInput,
      outputType: () => [BlogPost],
      permissions: [{ action: Action.Read, resource: 'BlogPost' }]
    })
    .addMutation({
      name: 'publishPost',
      methodName: 'publish',
      inputType: PublishPostInput,
      outputType: () => BlogPost,
      permissions: [{ action: Action.Update, resource: 'BlogPost' }]
    })
    .addResolveField({
      name: 'getPostAnalytics',
      methodName: 'getAnalytics',
      resolveField: 'analytics',
      outputType: () => PostAnalytics,
      permissions: [{ action: Action.Read, resource: 'BlogPost' }]
    })
    .and()
  // Custom subscription filtering
  .withSubscription({
    filter: BlogPostSubscriptionFilter,
    resolver: BlogPostSubscriptionResolver
  })

🔑 Key Concepts

Dynamic Module Generation

The library uses NestJS's dynamic module system to generate CRUD modules on-the-fly:

  • Each entity gets a dedicated dynamically-generated module with proper naming for debugging
  • The module contains all necessary providers (services, resolvers) with proper dependency injection
  • Generated modules are incorporated into your main NestJS application via the .forRoot() method

Data Providers

The DataProvider interface serves as an abstraction layer between your CRUD operations and database:

interface DataProvider {
  findOne<EntityType, WhereInputType>(modelName: string, ability: AppAbilityType, where: WhereInputType, select: Record<string, boolean>): Promise<EntityType | null>;
  findMany<EntityType, WhereInputType>(modelName: string, ability: AppAbilityType, args: {
    where: WhereInputType;
    pagination?: { take?: number; skip?: number };
    orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>;
    cursor?: any;
    distinct?: string[];
  }, select: Record<string, boolean>): Promise<EntityType[]>;
  create<EntityType, CreateInputType>(modelName: string, data: CreateInputType, select: Record<string, boolean>): Promise<EntityType>;
  update<EntityType, UpdateInputType>(modelName: string, ability: AppAbilityType, data: UpdateInputType, whereId: string, select: Record<string, boolean>): Promise<EntityType>;
  updateMany<EntityType, UpdateInputType, WhereInputType>(modelName: string, ability: AppAbilityType, data: UpdateInputType, where: WhereInputType, select: Record<string, boolean>): Promise<EntityType[]>;
  delete<EntityType>(modelName: string, ability: AppAbilityType, whereId: string, select: Record<string, boolean>): Promise<EntityType>;
  deleteMany<EntityType, WhereInputType>(modelName: string, ability: AppAbilityType, where: WhereInputType, select: Record<string, boolean>): Promise<EntityType[]>;
}

The built-in PrismaDataProvider:

  • Integrates with Prisma ORM for database operations
  • Applies CASL authorization rules directly in database queries using accessibleBy
  • Handles transactions and entity fetching optimally
  • NEW: Supports advanced query features like cursor pagination, complex ordering, and distinct queries

Field Selection

The FieldSelectionProvider optimizes performance by ensuring only requested GraphQL fields are fetched:

interface FieldSelectionProvider {
  parseSelection<EntityType>(info: GraphQLResolveInfo): FieldSelectionResult<EntityType>;
}

The built-in PrismaFieldSelectionProvider:

  • Uses @paljs/plugins to convert GraphQL selections to Prisma select objects
  • Handles nested field selection for relations
  • Prevents overfetching of data from the database

Advanced Query Features

The library now supports two query modes:

1. Simple Mode (Default)

Uses basic take/skip pagination:

// No additional configuration needed
CrudModulesFactory.forEntity(User).withConfig({...})

2. Advanced Mode (Opt-in)

Supports full Prisma-style querying:

// Enable advanced features
CrudModulesFactory.forEntity(User)
  .withConfig({...})
  .withFindManyArgs(FindManyUserArgs) // Generated by prisma-nest-graphql

Advanced mode enables:

  • Cursor-based pagination: More efficient for large datasets
  • Complex ordering: Multiple field sorting with direction
  • Distinct queries: Remove duplicates based on specific fields
  • Relay pagination patterns: Standards-compliant pagination

Relation Resolvers

The library supports three types of entity relationships:

  1. One-to-Many Relations (addRelation):

    .addRelation({
      fieldName: 'posts',         // GraphQL field name
      targetModel: 'post',        // Target Prisma model name
      targetType: Post,           // Target entity class
      targetWhereInput: PostWhere, // Optional filter input
      whereNullable: true,        // Is filter optional?
      relationField: 'authorId'   // Foreign key on target
    })
  2. One-to-One Relations (addOneToOneRelation):

    .addOneToOneRelation({
      fieldName: 'profile',      // GraphQL field name
      targetModel: 'profile',    // Target Prisma model name
      targetType: Profile,       // Target entity class
      relationField: 'profileId' // Foreign key on parent entity
    })
  3. Custom Relations (addCustomRelation / addCustomArrayRelation):

    • For many-to-many relations requiring join tables
    • For computed or virtual relations
    • Uses a custom resolver class implementing CustomRelationResolver:
    interface CustomRelationResolver<Item, Target, WhereInput> {
      resolve(ability: AppAbilityType, item: Item, context: any, args?: FindManyContract<WhereInput>): Promise<Target | Target[]>;
    }

Authorization

The library integrates with @eleven-am/authorizer (based on CASL) for access control:

  • Applies @CanPerform decorators to all generated resolvers automatically
  • Maps GraphQL operations to CASL actions:
    • Queries → Action.Read
    • Create mutations → Action.Create
    • Update mutations → Action.Update
    • Delete mutations → Action.Delete
  • Uses the current user's ability context within data provider operations
  • Translates CASL rules to database filters (e.g., in Prisma provider)

Subscriptions

Real-time updates via GraphQL subscriptions are built in:

  • Each entity gets a subscription resolver that emits events on entity changes
  • Uses graphql-subscriptions package's PubSub implementation
  • Services automatically publish events for create/update/delete operations
  • Optional custom filtering using SubscriptionResolver interface:
    interface SubscriptionResolver<EntityType, FilterType> {
      filter(filter: FilterType, changes: EntityType[]): boolean;
      resolve(filter: FilterType, changes: EntityType[]): Promise<EntityType[]>;
    }

Type Inference

The library leverages TypeScript's type inference system:

  • Types flow naturally between entity definitions, input types, and resolved outputs
  • No need to specify generic type parameters in most cases
  • Configuration objects maintain type safety while minimizing verbosity
  • Provides strong typing for returned entity objects in your application code

🔄 Migration Guide

Upgrading to Advanced Query Support

If you're upgrading from a previous version and want to use advanced query features:

Option 1: Keep existing behavior (no changes required)

Your existing code will continue to work exactly as before:

// This continues to work with take/skip pagination
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })

Option 2: Upgrade to advanced features

Add the withFindManyArgs() method to enable advanced query features:

// Import your generated FindManyArgs (from prisma-nest-graphql or similar)
import { FindManyUserArgs } from './generated/find-many-user.args';

CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })
  .withFindManyArgs(FindManyUserArgs) // 🎉 Add this line

Update your DataProvider (if custom)

If you have a custom DataProvider implementation, update the findMany method signature to support the new parameters:

// Before
async findMany<EntityType, WhereInputType>(
  modelName: string,
  ability: AppAbilityType,
  args: { where: WhereInputType; pagination?: PaginationContract },
  select: Record<string, boolean>
): Promise<EntityType[]>

// After
async findMany<EntityType, WhereInputType>(
  modelName: string,
  ability: AppAbilityType,
  args: {
    where: WhereInputType;
    pagination?: PaginationContract;
    orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>;
    cursor?: any;
    distinct?: string[];
  },
  select: Record<string, boolean>
): Promise<EntityType[]>

The built-in PrismaDataProvider already supports these features automatically.

🔧 Tool Integration

Working with prisma-nest-graphql

This library works seamlessly with prisma-nest-graphql for a complete code generation workflow:

  1. Generate Prisma schema artifacts:
npx prisma generate
  1. Generate NestJS GraphQL classes:
# Using prisma-nest-graphql generator
# Generates entity classes, input types, and FindManyArgs classes
  1. Configure CRUD modules with generated types:
// All these types are auto-generated by prisma-nest-graphql
import { User } from './generated/user/user.model';
import { UserCreateInput } from './generated/user/user-create.input';
import { UserUpdateInput } from './generated/user/user-update.input';
import { UserUpdateManyMutationInput } from './generated/user/user-update-many-mutation.input';
import { UserWhereInput } from './generated/user/user-where.input';
import { FindManyUserArgs } from './generated/user/find-many-user.args';

CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyMutationInput,
    whereInput: UserWhereInput,
  })
  .withFindManyArgs(FindManyUserArgs) // Generated FindManyArgs with full Prisma features

Working with GraphQL Code First

For code-first GraphQL development without generators:

// Define your own FindManyArgs if desired
@ArgsType()
export class CustomUserFindManyArgs {
  @Field(() => UserWhereInput, { nullable: true })
  where?: UserWhereInput;

  @Field(() => [UserOrderByInput], { nullable: true })
  orderBy?: UserOrderByInput[];

  @Field(() => UserWhereUniqueInput, { nullable: true })
  cursor?: UserWhereUniqueInput;

  @Field(() => Int, { nullable: true })
  take?: number;

  @Field(() => Int, { nullable: true })
  skip?: number;

  @Field(() => [UserScalarFieldEnum], { nullable: true })
  distinct?: UserScalarFieldEnum[];
}

// Use in configuration
CrudModulesFactory.forEntity(User)
  .withConfig({...})
  .withFindManyArgs(CustomUserFindManyArgs)

🎯 Use Cases

E-commerce Platform

// Product catalog with advanced filtering and relations
CrudModulesFactory.forEntity(Product)
  .withConfig({
    modelName: 'product',
    createInput: ProductCreateInput,
    updateInput: ProductUpdateInput,
    updateManyInput: ProductUpdateManyInput,
    whereInput: ProductWhereInput,
  })
  .withFindManyArgs(FindManyProductArgs) // Enable complex filtering, sorting
  .addRelation({
    fieldName: 'reviews',
    targetModel: 'review',
    targetType: Review,
    targetWhereInput: ReviewWhereInput,
    whereNullable: true,
    relationField: 'productId'
  })
  .addOneToOneRelation({
    fieldName: 'category',
    targetModel: 'category',
    targetType: Category,
    relationField: 'categoryId'
  })
  .withCustomResolver(ProductBusinessLogic)
    .addQuery({
      name: 'searchProducts',
      methodName: 'search',
      inputType: ProductSearchInput,
      outputType: () => [Product],
      permissions: [{ action: Action.Read, resource: 'Product' }]
    })
    .addResolveField({
      name: 'getAverageRating',
      methodName: 'calculateAverageRating',
      resolveField: 'averageRating',
      outputType: () => Number,
      permissions: [{ action: Action.Read, resource: 'Product' }]
    })
    .and()

Social Media Platform

// User profiles with complex friend relationships
CrudModulesFactory.forEntity(User)
  .withConfig({
    modelName: 'user',
    createInput: UserCreateInput,
    updateInput: UserUpdateInput,
    updateManyInput: UserUpdateManyInput,
    whereInput: UserWhereInput,
  })
  .withFindManyArgs(FindManyUserArgs)
  .addRelation({
    fieldName: 'posts',
    targetModel: 'post',
    targetType: Post,
    targetWhereInput: PostWhereInput,
    whereNullable: true,
    relationField: 'authorId'
  })
  .addCustomArrayRelation({
    fieldName: 'friends',
    targetModel: 'user',
    targetType: User,
    targetWhereInput: UserWhereInput,
    whereNullable: true,
    factoryClass: UserFriendsResolver // Custom many-to-many logic
  })
  .withSubscription({
    filter: UserActivityFilter,
    resolver: UserActivitySubscriptionResolver
  })

Content Management System

// Blog posts with advanced publishing workflow
CrudModulesFactory.forEntity(BlogPost)
  .withConfig({
    modelName: 'blogPost',
    createInput: BlogPostCreateInput,
    updateInput: BlogPostUpdateInput,
    updateManyInput: BlogPostUpdateManyInput,
    whereInput: BlogPostWhereInput,
  })
  .withFindManyArgs(FindManyBlogPostArgs) // Complex filtering by tags, categories, status
  .withCustomResolver(BlogPostWorkflow)
    .addMutation({
      name: 'publishPost',
      methodName: 'publish',
      inputType: PublishPostInput,
      outputType: () => BlogPost,
      permissions: [{ action: Action.Update, resource: 'BlogPost' }]
    })
    .addMutation({
      name: 'schedulePost',
      methodName: 'schedule',
      inputType: SchedulePostInput,
      outputType: () => BlogPost,
      permissions: [{ action: Action.Update, resource: 'BlogPost' }]
    })
    .addQuery({
      name: 'getAnalytics',
      methodName: 'getPostAnalytics',
      inputType: AnalyticsInput,
      outputType: () => PostAnalytics,
      permissions: [{ action: Action.Read, resource: 'BlogPost' }]
    })
    .and()

📊 Performance Considerations

Field Selection Optimization

The library automatically optimizes database queries by only selecting requested GraphQL fields:

# This query...
query {
  userFindMany {
    id
    name
    posts {
      title
    }
  }
}

# ...translates to optimized Prisma query:
# prisma.user.findMany({
#   select: {
#     id: true,
#     name: true,
#     posts: {
#       select: {
#         title: true
#       }
#     }
#   }
# })

Cursor Pagination for Large Datasets

When using withFindManyArgs(), leverage cursor-based pagination for better performance:

query {
  userFindMany(
    cursor: { id: "last_user_id" }
    take: 20
    orderBy: [{ createdAt: desc }]
  ) {
    id
    name
    createdAt
  }
}

Authorization Query Optimization

CASL rules are applied at the database level, not in application memory:

// Authorization rules become database WHERE conditions
// Instead of fetching all records and filtering in memory,
// the query becomes:
// SELECT * FROM users WHERE role != 'ADMIN' AND team_id = current_user.team_id

🐛 Troubleshooting

Common Issues

1. "Custom resolver class not registered" Error

Make sure to call withCustomResolver() before adding custom resolvers:

// ❌ Wrong
.addQuery({...}) // Error: no custom resolver registered

// ✅ Correct
.withCustomResolver(MyResolverClass)
  .addQuery({...})

2. Field Selection Not Working

Ensure your DataProvider uses the select parameter:

// ❌ Wrong - ignores field selection
async findMany(modelName: string, ability: any, args: any, select: any) {
  return this.prisma[modelName].findMany({
    where: args.where
    // Missing: ...select
  });
}

// ✅ Correct - applies field selection
async findMany(modelName: string, ability: any, args: any, select: any) {
  return this.prisma[modelName].findMany({
    where: args.where,
    ...select // Apply field selection for optimization
  });
}

3. Advanced Query Features Not Working

Make sure you've added withFindManyArgs() and updated your DataProvider:

// ❌ Wrong - missing configuration
CrudModulesFactory.forEntity(User)
  .withConfig({...})
  // Missing .withFindManyArgs()

// ✅ Correct - enables advanced features
CrudModulesFactory.forEntity(User)
  .withConfig({...})
  .withFindManyArgs(FindManyUserArgs)

4. Authorization Not Working

Verify your authorization setup:

// Ensure you have authorization providers
.withAuthorization(UserPolicies)

// And that your DataProvider applies CASL rules
const accessibleRecords = accessibleBy(ability, Action.Read).user;

📄 License

MIT

👤 Author

Roy OSSAI

🤝 Contributing

Contributions, issues, and feature requests are welcome! Please open an issue on the GitHub repository to discuss changes.

📞 Support

For questions or support, please open an issue on the GitHub repository.