MCPcopy Index your code
hub / github.com/Adrinalin4ik/Nestjs-Graphql-Tools

github.com/Adrinalin4ik/Nestjs-Graphql-Tools @0.10.2

Chat with this repo
repository ↗ · DeepWiki ↗ · release 0.10.2 ↗ · + Follow
325 symbols 496 edges 115 files 0 documented · 0%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

NestJS Graphql tools Logo

NestJS graphql Tools

NestJS Graphql automation library for building performant API

NPM Version Package License NPM Downloads mutable.ai

Description

The library allows to build efficient graphql API helping overcome n+1 problem and building hasura-like search interface with the minimum dependencies.

Introduction

With the library you will be able to build queries like that easily, using decorators and having full controll over everything.

{
  users(
    where: {
      id: { in: [1,2,3,4] }
      task_title: { like: "%Task%" }
    }
    order_by: {email: ASC, created_at: DESC}
    paginate: {page: 1, per_page: 10}
  ) {
    id
    fname
    lname
    email
    tasks(order_by: {id: ASC_NULLS_LAST}) {
      id
      title
    }
  }
}

Overview

Installation

npm i nestjs-graphql-tools
or
yarn add nestjs-graphql-tools

Data Loader n+1 problem solver

Loader usage guide

  1. Decorate your resolver with @GraphqlLoader()
  2. Add @Loader() parameter as a first parameter
  3. @Loader will return you LoaderData interface which includes ids of entities and helpers for constructing sutable object for graphql
One to many example
@Resolver(() => UserObjectType) 
export class UserResolver {

  @ResolveField(() => TaskObjectType)
  @GraphqlLoader()
  async tasks(
    @Loader() loader: LoaderData<TaskObjectType, number>,
    @Args('story_points') story_points: number, // custom search arg
  ) {
    const tasks = await getRepository(Task).find({
      where: {
        assignee_id: In<number>(loader.ids) // assignee_id is foreign key from Task to User table
        story_points
      }
    });

    return loader.helpers.mapOneToManyRelation(tasks, loader.ids, 'assignee_id'); // this helper will construct an object like { <assignee_id>: Task }. Graphql expects this shape.
  }
}
Many to one relation
@Resolver(() => TaskObjectType)
export class TaskResolver {

  constructor(
    @InjectRepository(User) public readonly userRepository: Repository<User>
  ) {}

  @ResolveField(() => UserObjectType)
  @GraphqlLoader({
    foreignKey: 'assignee_id' // Here we're providing foreigh key. Decorator gather all the keys from parent and provide it in loader.ids
  })
  async assignee(
    @Loader() loader: LoaderData<TaskObjectType, number>,
    @Filter(() => UserObjectType) filter: Brackets,
  ) {
    const qb = this.userRepository.createQueryBuilder('u')
      .where(filter)
      .andWhere({
        id: In(loader.ids) // Here will be assigne_ids
      })
    const users = await qb.getMany();
    return loader.helpers.mapManyToOneRelation(users, loader.ids); // This helper provide the shape {assignee_id: User}
  }
}

Polymorphic relations

@GraphqlLoader decorator provides ability to preload polymorphic relations

Usage

To be able to use it you need to decorate your resolver with @GraphqlLoader decorator. Decorator has parameter which allows to specify fields which needs to be gathered for polymorphic relation.

@GraphqlLoader({
  polymorphic: {
    idField: 'description_id', // Name of polymorphic id attribute of the parent model
    typeField: 'description_type' // Name of polymorphic type attribute of the parent model
  }
})

This decorator will aggregate all types and provide ids for each type. All aggregated types will be aveilable in @Loader decorator. It has attribute which called `polymorphicTypes.

PolmorphicTypes attribute shape

[
  {
    type: string | number
    ids: string[] | number[]
  }
]

Example
// Parent class
// task.resolver.ts
@Resolver(() => TaskObjectType)
export class TaskResolver {
  constructor(
    @InjectRepository(Task) public readonly taskRepository: Repository<Task>,
    @InjectRepository(Description) public readonly descriptionRepository: Repository<Description>
  ) {}

  @ResolveField(() => [DescriptionObjectType])
  @GraphqlLoader()
  async descriptions(
    @Loader() loader: LoaderData<TaskObjectType, number>,
    @SelectedUnionTypes({ 
      nestedPolymorphicResolverName: 'descriptionable',
    }) selectedUnions: SelectedUnionTypesResult // <-- This decorator will gather and provide selected union types. NestedPolymorphicResolverName argument allows to specify where specifically it should gather the fields
  ) {
    // Mapping graphql types to the database types
    const selectedTypes = Array.from(selectedUnions.types.keys()).map(type => { 
      switch (type) {
        case DescriptionTextObjectType.name:
          return DescriptionType.Text;
        case DescriptionChecklistObjectType.name:
          return DescriptionType.Checklist;
      }
    });

    const qb = this.descriptionRepository.createQueryBuilder('d')
      .andWhere({
        task_id: In(loader.ids),
        description_type: In(selectedTypes) // finding only selected types
      })

    const descriptions = await qb.getMany();
    return loader.helpers.mapOneToManyRelation(descriptions, loader.ids, 'task_id');
  }
}


// Polymorphic resolver
// description.resolver.ts
@Resolver(() => DescriptionObjectType)
export class DescriptionResolver {
  constructor(
    @InjectRepository(DescriptionText) public readonly descriptionTextRepository: Repository<DescriptionText>,
    @InjectRepository(DescriptionChecklist) public readonly descriptionChecklistRepository: Repository<DescriptionChecklist>,
  ) {}

  @ResolveField(() => [DescriptionableUnion], { nullable: true })
  @GraphqlLoader({ // <-- We will load description_id field of parent model to the ids and description_type field to the type
    polymorphic: {
      idField: 'description_id',
      typeField: 'description_type'
    }
  })
  async descriptionable(
    @Loader() loader: PolymorphicLoaderData<[DescriptionText | DescriptionChecklist], number, DescriptionType>, // <-- It will return aggregated polymorphicTypes
    @SelectedUnionTypes() types: SelectedUnionTypesResult // <-- It will extract from the query and return selected union types
  ) {
    const results = []; // <-- We need to gather all entities to the single array

    for (const item of loader.polimorphicTypes) {
      switch(item.descriminator) {
        case DescriptionType.Text:
          const textDescriptions = await this.descriptionTextRepository.createQueryBuilder()
          .select(types.getFields(DescriptionTextObjectType))
          .where({
            id: In(item.ids)
          })
          .getRawMany();

          results.push({ descriminator: DescriptionType.Text, entities: textDescriptions })

          break;
        case DescriptionType.Checklist:
          const checklistDescriptions = await this.descriptionChecklistRepository.createQueryBuilder()
          .select(types.getFields(DescriptionChecklistObjectType))
          .where({
            id: In(item.ids)
          })
          .getRawMany();

          results.push({ descriminator: DescriptionType.Checklist, entities: checklistDescriptions })

          break;
        default: break;
      }
    }
    return loader.helpers.mapOneToManyPolymorphicRelation(results, loader.ids); // <-- This helper will change shape of responce to the shape which is sutable for graphql
  }
}

You can find complete example in src/descriptions folder

Filters

Filter is giving ability to filter out entities by the condition. Condition looks similar to hasura interface using operators eq, neq, gt, gte, lt, lte, in, like, notlike, between, notbetween, null. By default it generates filter based on provided model. It supports only first level of the tables hierachy. If you need to search in depth you can declare custom filters (example 3).

Basic example 1

{
  users(where: {id: {eq: 1}}) {
    id
  }
}

Basic example 2

{
  users(
    where: {
      and: [
        {
          email: {like: "yahoo.com"}
        }
        {
          email: {like: "google.com"}
        }
      ],
      or: {
        id: {
          between: [1,2,3]
        }
      }
    }
  ) {
    id
  }
}

Filter usage guide

  1. Add @Filter() parameter with type of FilterArgs
  2. @Filter() will return typeorm compatible condition which you can use in your query builder.
@Query with filters
@Resolver(() => UserObjectType)
export class UserResolver {
  constructor(
    @InjectRepository(Task) public readonly taskRepository: Repository<Task>,
    @InjectRepository(User) public readonly userRepository: Repository<User>
  ) {}

  @Query(() => [UserObjectType])
  users(
    @Filter(() => UserObjectType) filter: FilterArgs, // It will return  typeorm condition
    @Args('task_title', {nullable: true}) taskTitle: string, // You can add custom additional filter if needed
  ) {
    const qb = this.userRepository.createQueryBuilder('u')
      .leftJoin('task', 't', 't.assignee_id = u.id')
      .where(filter)
      .distinct();

      if (taskTitle) { // mixed filters
        qb.andWhere(`t.title ilike :title`, { title: `%${taskTitle}%` })
      }

    return qb.getMany()
  }
}
@ResolveField with filter
@Resolver(() => UserObjectType)
export class UserResolver {
  constructor(@InjectRepository(Task) public readonly taskRepository: Repository<Task>) {}

  @ResolveField(() => TaskObjectType)
  @GraphqlLoader()
  async tasks(
    @Loader() loader: LoaderData<TaskObjectType, number>,
    @Filter(() => TaskObjectType) filter: FilterArgs,
  ) {
    const qb = this.taskRepository.createQueryBuilder()
    .where(filter)
    .andWhere({
      assignee_id: In<number>(loader.ids)
    });

    const tasks = await qb.getMany();

    return loader.helpers.mapOneToManyRelation(tasks, loader.ids, 'assignee_id');
  }
}
Custom filters
@InputType()
export class UserFilterInputType {
  @FilterField(() => String, { sqlExp: 't.title'})
  task_title: string;

  @FilterField(() => String, { sqlExp: 't.story_points'})
  task_story_points: number;

  @FilterField(() => String, { sqlExp: 'concat(u.fname, \' \', u.lname)'})
  full_name: string;
}

// Resolver
@Resolver(() => UserObjectType)
export class UserResolver {
  constructor(
    @InjectRepository(Task) public readonly taskRepository: Repository<Task>,
    @InjectRepository(StoryModel) public readonly storyRepository: Repository<StoryModel>,
    @InjectRepository(User) public readonly userRepository: Repository<User>
  ) {}

  @Query(() => [UserObjectType])
  users(
    @Filter(() => [UserObjectType, UserFilterInputType]) filter: FilterArgs, // <-- Object model and Filter model. It is possible to provide only one model or more that 2.
    @Sorting(() => UserObjectType, { sqlAlias: 'u' }) sorting: SortArgs<UserObjectType>
  ) {
    const qb = this.userRepository.createQueryBuilder('u')
      .leftJoin('task', 't', 't.assignee_id = u.id')
      .where(filter)
      .orderBy(sorting);

    return qb.getMany()
  }
}

You can also exclude some fields from the DTO filter. Read Exclusions.

Raw filters

Raw filters allow to get access to the user provided raw value right from the code. This feature allows to build your own filters interpreters.

How to use raw filters: 1. Add @Filter({ raw: true }) parameter with type of RawFilterArgs<T> where T is your filter type 2. @Filter() will return raw filter data.

```typesript export class UserResolver { @Query(() => [UserObjectType]) async usersRaw(

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Class 146
Function 73
Interface 53
Method 37
Enum 16

Languages

TypeScript100%

Modules by API surface

lib/filters/input-type-generator.ts15 symbols
dist-lib/filters/input-type-generator.js11 symbols
lib/loader.ts10 symbols
src/entities/user/user.resolver.ts9 symbols
lib/sorting/input-type-generator.ts9 symbols
lib/sorting/decorators/field.decorator.ts9 symbols
lib/filters/decorators/resolver.decorator.ts9 symbols
lib/sorting/decorators/resolver.decorator.ts8 symbols
lib/filters/decorators/field.decorator.ts8 symbols
src/entities/user/user.dto.ts7 symbols
src/database/migrations/1671015219018-seeds.ts7 symbols
lib/union-type-extractor.ts7 symbols

For agents

$ claude mcp add Nestjs-Graphql-Tools \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page