Skip to content

NazariiStrohush/gql-prisma-select

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

70 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

GQLPrismaSelect

CI npm version Test Coverage

Eliminate GraphQL over-fetching and under-fetching with automatic Prisma select/include generation

GQLPrismaSelect automatically converts your GraphQL query selections into optimized Prisma select and include objects. This ensures your database queries only fetch the data you actually need, improving performance and reducing bandwidth usage.

✨ Key Benefits

  • πŸš€ Performance: Fetch only the data your GraphQL queries request, including nested data
  • 🎯 Precision: Eliminate over-fetching and under-fetching automatically
  • πŸŽ›οΈ Arguments Support: Pass Prisma arguments (take, skip, orderBy) directly in GraphQL queries
  • πŸ”§ Simple: Drop-in replacement for manual select/include objects
  • πŸ› οΈ Flexible: Support for nested relations, advanced fragment handling, and custom transformations
  • 🎨 Modern: Built for Apollo Server and Prisma ecosystems

πŸ“‹ Table of Contents

πŸš€ Quick Start

Get started in 3 simple steps:

1. Install

npm install @nazariistrohush/gql-prisma-select

2. Import and Use

import { GQLPrismaSelect } from '@nazariistrohush/gql-prisma-select';

@Query(() => User)
async user(@Info() info: GraphQLResolveInfo, @Args('id') id: number) {
  const { include, select } = new GQLPrismaSelect(info);

  return this.prisma.user.findUnique({
    where: { id },
    include,
    select,
  });
}

3. Query with Precision

query {
  user(id: 1) {
    id
    email
    posts {
      id
      title
    }
  }
}

That's it! Your Prisma query now only fetches id, email from users and id, title from posts - no more, no less.

✨ Features

GQLPrismaSelect provides a comprehensive set of features to optimize your GraphQL-Prisma integration:

Core Features

Advanced Features

Developer Experience

πŸ“¦ Installation

npm install @nazariistrohush/gql-prisma-select

Prerequisites

  • Node.js: >= 14.16.0
  • Prisma: Any recent version
  • GraphQL Server: Apollo Server or compatible

Peer Dependencies

npm install prisma @apollo/server graphql

πŸ”§ Basic Usage

Constructor

The GQLPrismaSelect constructor takes a GraphQLResolveInfo object and optional configuration:

new GQLPrismaSelect(info, options?)

Parameters:

  • info: GraphQLResolveInfo - The GraphQL resolve info object from your resolver
  • options: Object (optional) - Configuration options

Returns:

  • include: Prisma include object for relations
  • select: Prisma select object for scalar fields
  • originalInclude: Untransformed include object
  • originalSelect: Untransformed select object

Basic Example

import { GQLPrismaSelect } from '@nazariistrohush/gql-prisma-select';

@Resolver(() => User)
export class UserResolver {
  constructor(private prisma: PrismaClient) {}

  @Query(() => User)
  async user(@Info() info: GraphQLResolveInfo, @Args('id') id: number) {
    const { include, select } = new GQLPrismaSelect(info);

    return this.prisma.user.findUnique({
      where: { id },
      include,
      select,
    });
  }
}

Nested Relations

GQLPrismaSelect automatically handles nested relations:

@Query(() => User)
async userWithPosts(@Info() info: GraphQLResolveInfo, @Args('id') id: number) {
  const { include, select } = new GQLPrismaSelect(info);

  // This will include posts and their authors if requested in the query
  return this.prisma.user.findUnique({
    where: { id },
    include,
    select,
  });
}

Prisma Arguments Support (New! ✨)

One of the best features of gql-prisma-select is the ability to pass Prisma arguments like take, skip, orderBy, where, and distinct directly from your GraphQL query. This works for both nested relations and root-level fields.

GraphQL Query:

query {
  users(take: 10, orderBy: { createdAt: desc }) {
    id
    email
    posts(take: 5, skip: 1, orderBy: { title: asc }) {
      title
    }
  }
}

Resulting Prisma Query (Automatically Generated):

// Nested arguments are automatically injected into select/include
{
  select: {
    id: true,
    email: true,
    posts: {
      select: { title: true },
      take: 5,
      skip: 1,
      orderBy: { title: 'asc' }
    }
  }
}

// Root arguments are available via .args property
const gqlSelect = new GQLPrismaSelect(info);
// gqlSelect.args = { take: 10, orderBy: { createdAt: 'desc' } }

Field Exclusion

By default, __typename fields are excluded. Add custom exclusions:

const { include, select } = new GQLPrismaSelect(info, {
  excludeFields: ['__typename', 'internalField']
});

Path-based Selection

Extract specific parts of selections using paths:

const selector = new GQLPrismaSelect(info);

// Get selection for User relation
const { include: userInclude, select: userSelect } = GQLPrismaSelect.get(
  selector,
  'posts.author' // lodash-style path
);

// Get selection for Post relation
const { include: postInclude, select: postSelect } = GQLPrismaSelect.get(
  selector,
  ['posts'] // array path
);

πŸš€ Advanced Features

Field Transformations

Transform field names between GraphQL and Prisma schemas:

const { include, select } = new GQLPrismaSelect(info, {
  transforms: {
    fieldTransforms: {
      // Map GraphQL field to different Prisma field
      'fullName': 'full_name',
      'createdAt': 'created_at'
    },
    defaultTransforms: ['camelToSnake'], // Auto-convert camelCase to snake_case
    transformRelations: true // Also transform relation fields
  }
});

Built-in transformers:

  • camelToSnake: userName β†’ user_name
  • snakeToCamel: user_name β†’ userName
  • pluralize: user β†’ users
  • singularize: users β†’ user

Custom Field Mapping

Map GraphQL fields to completely different Prisma fields:

const { include, select } = new GQLPrismaSelect(info, {
  transforms: {
    fieldTransforms: {
      'displayName': (value, context) => {
        // Custom logic for field transformation
        if (context.modelName === 'User') {
          return 'full_name';
        }
        return value;
      }
    }
  }
});

Fragments

GQLPrismaSelect fully supports GraphQL fragments:

fragment UserFields on User {
  id
  email
  profile {
    firstName
    lastName
  }
}

query {
  users {
    ...UserFields
    posts {
      id
      title
    }
  }
}

The library automatically resolves fragment selections into the appropriate Prisma select/include objects.

Advanced Fragment Features

GQLPrismaSelect now supports advanced fragment handling capabilities:

Fragment Registry & Caching

import { FragmentRegistry, FragmentCache } from '@nazariistrohush/gql-prisma-select';

// Register fragments for reuse
FragmentRegistry.register({
  name: 'UserBasic',
  type: 'User',
  selections: { id: true, email: true, name: true },
  metadata: { size: 30, complexity: 3, dependencies: [], usageCount: 0, lastUsed: new Date() }
});

// Use cached fragments
const cache = new FragmentCache({ enabled: true, ttl: 300000 });

Fragment Overrides

import { FragmentOverrider } from '@nazariistrohush/gql-prisma-select';

const override = {
  fragmentName: 'UserBasic',
  excludeFields: ['internalId'],
  includeFields: ['avatar'],
  transformFields: { 'fullName': 'display_name' },
  removeSelections: ['profile.private']
};

const customizedFragment = FragmentOverrider.apply(baseFragment, override);

Dynamic Fragments

import { DynamicFragmentHandler } from '@nazariistrohush/gql-prisma-select';

const dynamicFragments = [
  {
    name: 'AdminOnly',
    condition: (ctx) => ctx.user?.role === 'admin',
    selections: { adminData: true, permissions: true },
    priority: 1
  },
  {
    name: 'PremiumUser',
    condition: (ctx) => ctx.user?.subscription === 'premium',
    selections: { premiumFeatures: true, analytics: true },
    priority: 2
  }
];

const context = { user: { role: 'admin', subscription: 'premium' } };
const activeFragments = DynamicFragmentHandler.evaluate(dynamicFragments, context);

Fragment Optimization

import { FragmentOptimizer, FragmentAnalyzer } from '@nazariistrohush/gql-prisma-select';

// Optimize fragments for performance
const optimized = FragmentOptimizer.inline(fragment, 5); // Inline if used < 5 times

// Analyze fragment usage
const analysis = FragmentAnalyzer.analyze(allFragments);
console.log('Optimization suggestions:', analysis.opportunities);

Result Transformation

Transform query results after fetching:

const selector = new GQLPrismaSelect(info, {
  transforms: {
    resultTransforms: {
      'User.email': (value) => value.toLowerCase(),
      'Post.title': (value) => value.trim()
    }
  }
});

// Access transformed results
const result = await prisma.user.findUnique({...});
const transformedResult = selector.transformResult(result);

Schema-Aware Validation (Coming Soon)

Future versions will include Prisma schema validation:

const { include, select } = new GQLPrismaSelect(info, {
  schema: prismaSchema, // Validate against Prisma schema
  validateFields: true,
  optimizeQueries: true
});

Type-Safe Integration

GQLPrismaSelect provides advanced TypeScript utilities for compile-time type safety and enhanced developer experience.

TypedGQLPrismaSelect

Get full type safety with IntelliSense support for GraphQL and Prisma integration:

import { TypedGQLPrismaSelect } from '@nazariistrohush/gql-prisma-select';

interface GraphQLUser {
  id: string;
  name: string;
  email: string;
  posts: {
    id: string;
    title: string;
    content: string;
  };
}

export const userResolver = async (
  parent: any,
  args: { id: string },
  context: any,
  info: GraphQLResolveInfo
) => {
  const selector = new TypedGQLPrismaSelect<GraphQLUser, 'User'>(info);

  // Fully typed with IntelliSense
  const select = selector.getTypedSelect();
  const include = selector.getTypedInclude();

  return context.prisma.user.findUnique({
    where: { id: args.id },
    select,
    include
  });
};

Runtime Type Validation

Validate selections against your GraphQL schema at runtime:

import { TypedGQLPrismaSelect } from '@nazariistrohush/gql-prisma-select';

const selector = new TypedGQLPrismaSelect<User, 'User'>(info, {
  typeValidation: {
    strict: true,
    validateEnums: true,
    validateRelations: true
  }
});

// Validate selections against schema
const validationResult = selector.validateTypes(schema);
if (!validationResult.isValid) {
  console.error('Type validation errors:', validationResult.errors);
}

Typed Query Builder

Build type-safe Prisma queries with a fluent API:

import { TypedQueryBuilder } from '@nazariistrohush/gql-prisma-select';

const builder = new TypedQueryBuilder({ model: 'User' });

const query = builder
  .select({ id: true, name: true, email: true })
  .include({
    posts: {
      select: { id: true, title: true },
      include: { comments: true }
    }
  })
  .where({ active: true })
  .orderBy({ createdAt: 'desc' })
  .build();

// query is fully typed and ready for Prisma
const users = await prisma.user.findMany(query);

Schema Type Generation

Generate TypeScript types from your GraphQL schema:

import { TypeGenerator } from '@nazariistrohush/gql-prisma-select';
import { buildSchema } from 'graphql';

const schema = buildSchema(`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
`);

const generator = new TypeGenerator(schema, {
  output: './generated',
  generateQueries: true,
  generateMutations: true
});

// Generate all types
await generator.generate();

Library Integrations

Built-in integrations for popular GraphQL libraries:

// Nexus Integration
import { NexusIntegration } from '@nazariistrohush/gql-prisma-select';

const UserQuery = NexusIntegration.createQueryField<'User'>({
  type: 'User',
  args: { id: idArg() },
  model: 'User',
  resolve: async (root, args, ctx, info) => {
    const selector = new TypedGQLPrismaSelect<any, 'User'>(info);
    const { select, include } = selector.getTypedSelect();

    return ctx.prisma.user.findUnique({
      where: { id: args.id },
      select,
      include
    });
  }
});

// Apollo Server Integration
import { ApolloServerIntegration } from '@nazariistrohush/gql-prisma-select';

const resolvers = ApolloServerIntegration.createResolvers({
  Query: {
    user: ApolloServerIntegration.createQueryResolver<'User'>(
      'User',
      async (args, context, info) => {
        const selector = new TypedGQLPrismaSelect<any, 'User'>(info);
        const select = selector.getTypedSelect();

        return context.prisma.user.findUnique({
          where: { id: args.id },
          ...select
        });
      }
    )
  }
});

Framework Integration

NestJS (Code First)

@Resolver(() => User)
export class UserResolver {
  @Query(() => User)
  async user(@Info() info: GraphQLResolveInfo) {
    const { include, select } = new GQLPrismaSelect(info);
    return this.prisma.user.findUnique({ include, select });
  }
}

Apollo Server (Schema First)

const resolvers = {
  Query: {
    user: async (parent, args, context, info) => {
      const { include, select } = new GQLPrismaSelect(info);
      return prisma.user.findUnique({ where: { id: args.id }, include, select });
    }
  }
};

Express GraphQL

const root = {
  user: async (args, context, info) => {
    const { include, select } = new GQLPrismaSelect(info);
    return prisma.user.findUnique({ where: { id: args.id }, include, select });
  }
};

πŸ“š Examples

Basic User Query

Prisma Schema:

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

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

GraphQL Schema:

@ObjectType()
class User {
  @Field(() => Int)
  id: number;

  @Field(() => String)
  email: string;

  @Field(() => String)
  firstName: string;

  @Field(() => String)
  lastName: string;

  @Field(() => [Post])
  posts: Post[];
}

@ObjectType()
class Post {
  @Field(() => Int)
  id: number;

  @Field(() => String)
  title: string;

  @Field(() => String)
  content: string;

  @Field(() => User)
  author: User;
}

Resolver:

@Resolver(() => User)
export class UserResolver {
  constructor(private prisma: PrismaClient) {}

  @Query(() => User)
  async user(@Info() info: GraphQLResolveInfo, @Args('id') id: number) {
    const { include, select } = new GQLPrismaSelect(info);
    return this.prisma.user.findUnique({
      where: { id },
      include,
      select,
    });
  }
}

Complex Nested Queries

Query:

query GetUserDetails($userId: Int!) {
  user(id: $userId) {
    id
    email
    firstName
    lastName
    posts {
      id
      title
      content
      author {
        id
        email
      }
    }
  }
}

Generated Prisma Query:

{
  include: {
    posts: {
      include: {
        author: {
          select: {
            id: true,
            email: true
          }
        }
      },
      select: {
        id: true,
        title: true,
        content: true
      }
    }
  },
  select: {
    id: true,
    email: true,
    firstName: true,
    lastName: true
  }
}

Field Name Transformations

Use Case: GraphQL uses camelCase, Prisma uses snake_case

const { include, select } = new GQLPrismaSelect(info, {
  transforms: {
    defaultTransforms: ['camelToSnake'],
    transformRelations: true
  }
});

// GraphQL: firstName, lastName, createdAt
// Prisma: first_name, last_name, created_at

Fragments Example

fragment UserBasic on User {
  id
  email
  firstName
  lastName
}

fragment PostDetails on Post {
  id
  title
  content
  author {
    ...UserBasic
  }
}

query GetPosts {
  posts {
    ...PostDetails
    createdAt
  }
}

The library automatically expands fragments into the full selection set.

Custom Field Mapping

const { include, select } = new GQLPrismaSelect(info, {
  transforms: {
    fieldTransforms: {
      'fullName': 'full_name',           // Simple mapping
      'displayName': (value, context) => { // Custom function
        return context.modelName === 'User' ? 'full_name' : value;
      }
    }
  }
});

πŸ“– API Reference

Constructor

new GQLPrismaSelect(info: GraphQLResolveInfo, options?: GQLPrismaSelectOptions)

Options

Option Type Description Default
excludeFields string[] Fields to exclude from selection ['__typename']
get string | string[] Path to extract specific selection undefined
transforms TransformOptions Field and result transformation options undefined

TransformOptions

interface TransformOptions {
  fieldTransforms?: FieldTransforms;           // Custom field mappings
  defaultTransforms?: TransformType[];         // Built-in transformers
  transformRelations?: boolean;                // Transform relation fields
  transformEnums?: boolean;                    // Transform enum values
  caseSensitive?: boolean;                     // Case sensitivity
  customTransformers?: Record<string, Function>; // Custom transformer functions
}

Return Values

Property Type Description
include object Prisma include object for relations
select object Prisma select object for scalar fields
args object Extracted root-level arguments (take, skip, etc.)
originalInclude object Include object without transformations
originalSelect object Select object without transformations

Static Methods

GQLPrismaSelect.get(selection, path)

Extracts specific parts of selections by path.

GQLPrismaSelect.get(selector, 'posts.author'); // lodash-style path
GQLPrismaSelect.get(selector, ['posts', 'author']); // array path

Parameters:

  • selection: GQLPrismaSelect instance or selection object
  • path: string | string[] - Path to extract

Returns: { include, select } object for the specified path

Built-in Transformers

Transformer Input Output Example
camelToSnake userName user_name firstName β†’ first_name
snakeToCamel user_name userName last_name β†’ lastName
pluralize user users category β†’ categories
singularize users user categories β†’ category

Error Handling

The library throws descriptive errors for:

  • Invalid GraphQL resolve info
  • Circular fragment references
  • Invalid transformation configurations
  • Path extraction failures

πŸ”§ Type-Safe API Reference

TypedGQLPrismaSelect

new TypedGQLPrismaSelect<TGraphQL, TPrisma>(
  info: GraphQLResolveInfo,
  options?: TypedOptions<TGraphQL, TPrisma>
)

Advanced type-safe version of GQLPrismaSelect with full IntelliSense support.

Type Parameters:

  • TGraphQL: GraphQL type interface (e.g., { id: string; name: string })
  • TPrisma: Prisma model name as string literal (e.g., 'User')

Options:

Option Type Description Default
excludeFields string[] Fields to exclude from selection ['__typename']
transforms TransformOptions Field transformation options undefined
fragments FragmentOptions Fragment handling options undefined
typeValidation TypeValidationOptions Runtime type validation options undefined

Methods:

Method Returns Description
getTypedSelect() SafeSelect<TGraphQL, TPrisma> Type-safe select object with full IntelliSense
getTypedInclude() PrismaSelect<TPrisma> Type-safe include object for relations
validateTypes(schema?) ValidationResult Runtime validation against GraphQL schema
transformResultTyped(result) any Transform result with type validation

TypedQueryBuilder

new TypedQueryBuilder<TModel>(options: TypedQueryBuilderOptions<TModel>)

Fluent API for building type-safe Prisma queries.

Type Parameters:

  • TModel: Prisma model name as string literal (e.g., 'User')

Options:

Option Type Description Default
model TModel Prisma model name Required
schema GraphQLSchema GraphQL schema for validation undefined
typeValidation boolean Enable type validation true

Methods:

Method Returns Description
select(fields) this Add fields to select (type-safe)
include(relations) this Add relations to include (type-safe)
where(conditions) this Add where conditions
orderBy(order) this Add ordering
build() TypedPrismaQuery<TModel> Build complete query object
getModel() TModel Get model name

TypeGenerator

new TypeGenerator(schema: GraphQLSchema, options?: Partial<TypeGenerationOptions>)

Generate TypeScript types from GraphQL schemas.

Options:

Option Type Description Default
output string Output directory './generated'
generateQueries boolean Generate query types true
generateMutations boolean Generate mutation types true
generateSubscriptions boolean Generate subscription types true
customScalars Record<string, string> Custom scalar mappings {}
namespace string Type namespace 'Generated'

Methods:

Method Returns Description
generateTypes() TypeGenerationResult Generate all types
generateModelTypes() string Generate model types
generateQueryTypes() string Generate query types

Static Methods:

Method Returns Description
generate(options) Promise<void> Generate types to files
generateForModel(modelName, schema) string Generate types for specific model

TypeValidator

new TypeValidator(schema: GraphQLSchema, options?: TypeValidationOptions)

Runtime type validation against GraphQL schemas.

Options:

Option Type Description Default
strict boolean Throw on type mismatches false
warnOnMissing boolean Warn on missing fields true
validateEnums boolean Validate enum values true
validateRelations boolean Validate relation types true

Methods:

Method Returns Description
validate(value, type) ValidationResult Validate value against type
validateSelection(selection, type) ValidationResult Validate GraphQL selection
validateEnum(value, enumType) boolean Validate enum value
validateRelations(selections, parentType) ValidationResult Validate relations

Library Integrations

NexusIntegration

Method Description
createQueryField<TModel>(config) Create typed Nexus query field
createMutationField<TModel>(config) Create typed Nexus mutation field
createResolvers<TModel>(model, resolvers) Create typed field resolvers

ApolloServerIntegration

Method Description
createResolvers(resolvers) Create resolver map
createQueryResolver<TModel>(model, resolver) Create typed query resolver
createMutationResolver<TModel>(model, resolver) Create typed mutation resolver

LibraryRegistry

Method Description
register(name, integration) Register library integration
get(name) Get registered integration
list() List all registered integrations

πŸ”„ Migration Guide

From Manual Select/Include

Before (Manual):

@Query(() => User)
async user(@Args('id') id: number) {
  return this.prisma.user.findUnique({
    where: { id },
    select: {
      id: true,
      email: true,
      firstName: true,
      lastName: true,
      posts: {
        select: {
          id: true,
          title: true,
          content: true
        }
      }
    }
  });
}

After (GQLPrismaSelect):

@Query(() => User)
async user(@Info() info: GraphQLResolveInfo, @Args('id') id: number) {
  const { include, select } = new GQLPrismaSelect(info);
  return this.prisma.user.findUnique({
    where: { id },
    include,
    select,
  });
}

From Other Libraries

Comparison with graphql-fields

// graphql-fields approach
import graphqlFields from 'graphql-fields';

const fields = graphqlFields(info);
// Manual processing required...

// GQLPrismaSelect approach
const { include, select } = new GQLPrismaSelect(info);
// Ready for Prisma!

Benefits Over Manual Approaches

  • Automatic: No need to manually define select/include objects
  • Accurate: Fetches exactly what GraphQL requests
  • Maintainable: Changes to GraphQL schema automatically reflected
  • Type-Safe: Full TypeScript support
  • Flexible: Advanced features like transformations and fragments

Common Migration Patterns

1. Basic Queries

Replace static select objects with dynamic generation.

2. Nested Relations

The library handles all nesting automatically.

3. Field Filtering

Use excludeFields option instead of manual filtering.

4. Custom Logic

Use field transformations for complex mappings.

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

git clone https://github.com/NazariiStrohush/gql-prisma-select.git
cd gql-prisma-select
npm install
npm run build
npm test

Reporting Issues

  • Use GitHub Issues for bugs
  • Check existing issues before creating new ones
  • Include code examples and GraphQL schemas when possible

Feature Requests

  • Open a GitHub Issue with the "enhancement" label
  • Describe the use case and expected behavior
  • Consider submitting a pull request if you have implementation ideas

πŸ“„ License

ISC License - see the LICENSE file for details.

πŸ™ Acknowledgments

  • Built for the Apollo Server and Prisma ecosystems
  • Inspired by the need for better GraphQL-Prisma integration
  • Thanks to all contributors and the open-source community

About

GQL Info parser which converts your request to Prisma include or select object

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors