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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nestjs!/graphql-filter

v1.0.8

Published

GraphQL tools for Nest framework that gives a set of decorators and pipe transforms for make filtering, ordering, pagination and selection inputs easily for use with a ORM.

Downloads

42

Readme

Description

This is an unofficial Nest package that gives a set of decorators for facilitate the filtering, ordering, pagination and selection tasks when using GraphQL with a ORM.

The package includes decorators @FilterField, @FilterWhereType and @FilterOrderType for make Where and Order input types for GraphQL from entities definitions, and then they correspondent pipe transforms for convert the inputs to valid TypeORM repository find method parameters. Also there's included a @SelectionSet decorator, that can be used in resolver arguments, which returns a defined class object that gives methods for obtain the relationships that can be used in the TypeORM repository find method, or for authorization guards. As well is provided a PaginationInput type that gives a simple input type structure for receive pagination parameters.

Installation

$ npm install @nestjs!/graphql-filter

Usage

Code First

Schema

Query

Utils

InputTypes Reference

For explain the usage of the package utilities we explore a example with User, Profile, Session and Device entities.

Code First


First decorate the entities attributes you want to be filters with @FilterField decorator. And then, export Where and Order input classes with the @FilterWhereType and @FilterOrderType decorators referencing to the entity class:

Entities

user.entity.ts

import {
  Field,
  InputType,
  IntersectionType,
  ObjectType,
  OmitType,
  PartialType,
  PickType,
  registerEnumType
} from '@nestjs/graphql';

import { FilterField, FilterOrderType, FilterWhereType } from '@nestjs!/graphql-filter';

import { IsEmail, MaxLength, MinLength } from 'class-validator';
import { GraphQLEmailAddress, GraphQLUUID } from 'graphql-scalars';
import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  Unique,
  UpdateDateColumn
} from 'typeorm';

import { Profile, ProfileOrderInput, ProfileWhereInput } from './profile.entity';
import { Session, SessionOrderInput, SessionWhereInput } from 'src/sessions/entities/session.entity';

export enum Role {
  USER = 'user',
  ADMIN = 'admin'
}

registerEnumType(Role, {
  name: 'Role',
  description: 'Defines wich permissions user has.',
  valuesMap: {
    USER: {
      description: 'User role can access to application basic features.'
    },
    ADMIN: {
      description: 'Admin role can access to all application features.'
    }
  }
});

@ObjectType()
@InputType('UserInput', { isAbstract: true })
@Entity()
export class User {
  @Field(() => GraphQLUUID)
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Field()
  @FilterField()
  @Column()
  @Unique(['username'])
  @MinLength(4)
  @MaxLength(30)
  username: string;

  @Field(() => GraphQLEmailAddress)
  @FilterField()
  @Column()
  @Unique(['email'])
  @IsEmail()
  @MaxLength(100)
  email: string;

  @Field()
  @Column()
  @MinLength(8)
  @MaxLength(100)
  password: string;

  @Field(() => Role, { nullable: true })
  @FilterField()
  @Column({ type: 'enum', enum: Role, default: Role.USER })
  role: Role;

  @Field(() => Profile, { nullable: true })
  @FilterField(() => ProfileWhereInput, () => ProfileOrderInput)
  @Column(() => Profile)
  profile: Profile;

  @Field(() => [Session], { nullable: true })
  @FilterField(() => SessionWhereInput, () => SessionOrderInput)
  @OneToMany(() => Session, (session) => session.user, { cascade: true })
  sessions: Session[];
}

@InputType()
export class UserCreateInput extends OmitType(
  User,
  ['id', 'createdAt', 'updatedAt', 'deletedAt', 'sessions'],
  InputType
) {}

@InputType()
export class UserUpdateInput extends IntersectionType(
  PartialType(UserCreateInput),
  PickType(User, ['id'], InputType)
) {}

@FilterWhereType(User)
export class UserWhereInput {}

@FilterOrderType(User)
export class UserOrderInput {}

profile.entity.ts

...
import { FilterField, FilterOrderType, FilterWhereType, FloatWhereInput, IntWhereInput } from '@nestjs!/graphql-filter';

@ObjectType()
@InputType('ProfileInput')
export class Profile {
  @Field({ nullable: true })
  @FilterField()
  @Column({ nullable: true })
  name: string;

  @Field({ nullable: true })
  @FilterField()
  @Column({ nullable: true })
  bio: string;

  @Field(() => GraphQLInt)
  @FilterField(() => IntWhereInput)
  age: number

  @Field(() => GraphQLFloat)
  @FilterField(() => FloatWhereInput)
  height: number
}

@FilterWhereType(Profile)
export class ProfileWhereInput {}

@FilterOrderType(Profile)
export class ProfileOrderInput {}

session.entity.ts

...
import { FilterField, FilterOrderType, FilterWhereType } from '@nestjs!/graphql-filter';

import { Device, DeviceOrderInput, DeviceWhereInput } from './device.entity';
import { User, UserOrderInput, UserWhereInput } from 'src/users/entities/user.entity';

@ObjectType()
@InputType('SessionInput', { isAbstract: true })
@Entity()
export class Session {
  @Field(() => GraphQLUUID)
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Field(() => User, { nullable: true })
  @FilterField(() => UserWhereInput, () => UserOrderInput)
  @ManyToOne(() => User, (user) => user.sessions)
  user: User;

  @Field()
  @FilterField()
  @Column()
  token: string;

  @Field(() => Device, { nullable: true })
  @FilterField(() => DeviceWhereInput, () => DeviceOrderInput)
  @Column(() => Device)
  device: Device;
}

@FilterWhereType(Session)
export class SessionWhereInput {}

@FilterOrderType(Session)
export class SessionOrderInput {}

device.entity.ts

...
import { FilterField, FilterOrderType, FilterWhereType } from '@nestjs!/graphql-filter';

@ObjectType()
@InputType('DeviceInput')
export class Device {
  @Field({ nullable: true })
  @FilterField()
  @Column({ nullable: true })
  client: string;

  @Field({ nullable: true })
  @FilterField()
  @Column({ nullable: true })
  os: string;

  @Field({ nullable: true })
  @FilterField()
  @Column({ nullable: true })
  ip: string;
}

@FilterWhereType(Device)
export class DeviceWhereInput {}

@FilterOrderType(Device)
export class DeviceOrderInput {}

Resolvers

Now in the entities resolvers we can define a findMany method that receives the correspondent filtering, ordering, pagination and selection parameters and apply the pipe transforms for get, in this case, the TypeORM formatted parameters for use with the repository find method. For example for the Users resolver:

users.resolver.ts

...
import {
  PaginationInput,
  SelectionInput,
  SelectionSet,
  TypeORMOrderTransform,
  TypeORMWhereTransform
} from '@nestjs!/graphql-filter';

import { FindOptionsOrder, FindOptionsWhere } from 'typeorm';

import { User, UserCreateInput, UserOrderInput, UserUpdateInput, UserWhereInput } from './entities/user.entity';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  ...

  @Query(() => [User], { name: 'users', nullable: 'items' })
  async findMany(
    @Args('where', { type: () => [UserWhereInput], nullable: true }, TypeORMWhereTransform<User>)
    where: FindOptionsWhere<User>,
    @Args('order', { type: () => [UserOrderInput], nullable: true }, TypeORMOrderTransform<User>)
    order: FindOptionsOrder<User>,
    @Args('pagination', { nullable: true }) pagination: PaginationInput,
    @SelectionSet() selection: SelectionInput
  ) {
    return await this.usersService.findMany( where, order, pagination, selection );
  }
}

Services

Last step is define the correspondent services using the transformed parameters with the entity repository provided by TypeORM. For example for the Users service:

users.service.ts

...
import { PaginationInput, SelectionInput } from '@nestjs!/graphql-filter';

import { FindOptionsOrder, FindOptionsWhere, Repository } from 'typeorm';

import { User, UserCreateInput, UserUpdateInput } from './entities/user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>
  ) {}

  ...

  async findMany(
    where: FindOptionsWhere<User>,
    order: FindOptionsOrder<User>,
    pagination: PaginationInput,
    selection: SelectionInput
  ) {
    return await this.usersRepository.find({
      relations: selection?.getTypeORMRelations(),
      where: where,
      order: order,
      skip: pagination ? (pagination.page - 1) * pagination?.count : null,
      take: pagination ? pagination.count : null
    });
  }
}

Schema


With decorators defined the package will generate the correspondent GraphQL basic filtering, ordering and pagination input types and the specific entities input types at compilation time with the rest of types.

For example for User entity it will generate:

schema.gql

input UserWhereInput {
  email: StringWhereInput
  profile: ProfileWhereInput
  role: StringWhereInput
  username: StringWhereInput
  sessions: SessionWhereInput
}

input UserOrderInput {
  email: OrderDirection
  profile: ProfileOrderInput
  role: OrderDirection
  username: OrderDirection
  sessions: SessionOrderInput
}

Query


Now we can execute our Nest project and make GraphQL queries with the filtering, ordering and pagination parameters defined. For example for the defined users query:

QUERY

query Users($where: [UserWhereInput!], $order: [UserOrderInput!], $pagination: PaginationInput) {
  users(where: $where, order: $order, pagination: $pagination) {
    id
    username
    email
    role
    profile {
      name
      bio
      age
      height
    }
    sessions {
      id
      token
      device {
        client
        os
        ip
      }
    }
  }
}

GRAPHQL VARIABLES

{
  "where": [
    {
      "username": {
        "or": [
          {
            "ne": "something"
          },
          {
            "like": "%some%"
          }
        ]
      },
      "profile": {
        "name": {
          "ilike": "Georgy Something"
        }
      },
      "sessions": {
        "device": {
          "client": {
            "like": "%Postman%"
          }
        }
      }
    },
    {
      "role": {
        "eq": "admin"
      },
      "email": {
        "and": [
          {
            "in": ["[email protected]", "[email protected]"]
          },
          {
            "not": {
              "any": ["[email protected]"]
            }
          }
        ]
      }
    }
  ],
  "order": [
    {
      "username": "ASC"
    },
    {
      "email": "DESC"
    },
    {
      "profile": {
        "name": "ASC"
      }
    }
  ],
  "pagination": {
    "page": 2,
    "count": 10
  }
}

Here we can note that the "where" variable can be an array or a object, meaning that it's an OR clause or AND clause respectively.

For "order" variable something similar happen, we can define the variable as an array or object, but if it's a object we can't ensure the priority of the ordering parameters.

Utils

Many

When we want to create a findMany query where you want to return the result set aside with the total count of elements (which is a pretty common and efficient way of query the data), you have to create a custom ObjectType that allow send the data set along with the count number. Here is where the @Many decorator appears to automatize this task:

session.entity

@ObjectType()
// ...
export class Session {
  // ...
}

// ...

@Many(Session)
export class Sessions {}

This decorated class will generate the next ObjectType in the schema:

type Sessions {
  sessions: [Session!]
  count: Int!
}

Later our resolver can use the Sessions class for reference to this type:

sessions.resolver.ts

import { Sessions } from './entities/session.entity';

@Resolver(() => Session)
export class SessionsResolver {
    //...

    // we use 'Sessions' class as return type
    @Query(() => Sessions, { name: 'sessions' })
    async findMany(
        ...,
        // remember use the 'root' parameter for make SelectionInput using the set of sessions
        @SelectionSet({ root: 'sessions' }) selection: SelectionInput
    ) {
    return await this.sessionsService.findMany(..., selection);
    }
}

And finally the service function will be like this:

sessions.service.ts

async findMany(
    where: FindOptionsWhere<Session>,
    order: FindOptionsOrder<Session>,
    pagination: PaginationInput,
    selection: SelectionInput
  ) {
    // we use 'findAndCount' method instead of 'find'
    const [sessions, count] = await this.sessionsRepository.findAndCount({
      relations: selection?.getTypeORMRelations(),
      where: where,
      order: order,
      skip: pagination ? (pagination.page - 1) * pagination.count : null,
      take: pagination ? pagination.count : null
    });
    return { sessions, count };
  }

SelectionSet

When using the @SelectionSet decorator you will get a SelectionInput object which is obtained by processing the GraphQL context info and organized for easily accessing the selection.

From this object you have the next methods at disposition:

getAttributes(): returns a string array with only the entity attributes selected, it means that relationships are not included. For example, for the query of the next section we get:

["id", "username", "email", "role"]

getFullAttributes(): returns a string array with all the entity attributes selected and the relationships ones. For example, for the query of the next section we get:

[
  "id", "username", "email", "role", "profile.name", "profile.bio", "profile.age", "profile.height", "sessions.id", "sessions.token", "sessions.device.client", "sessions.device.os", "sessions.device.ip"
]

getRelations(include?): returns a object formatted like TypeORM repository find relations parameter. Also some extra relations can be added using include parameter with a valid TypeORM relations object. For example, for the query of the next section we get:

{
    profile: true,
    sessions: true
}

Note: it also return nested relations (if there nested subfields).

options

You can use the root option for define the initial node from where the SelectionInput will be generated. For example:

If you have this query:

query Sessions {
    sessions {
        collections {
            results {
                id
                objects {
                    id
                    ...
                }
                ...
            }
        }
        count
    }
}

And you want to receive only the data from results for compute the relations, fields, etc of the SelectionInput. You can use this on resolver:

@Query(() => Sessions, { name: 'sessions' })
async findMany(
...,
@SelectionSet({ root: 'collections.results' }) selection: SelectionInput
) {
return await this.sessionsService.findMany(..., selection);
}

AuthUser

When using the @AuthUser decorator you will get a object which is obtained from the GraphQL context request. You can also send a string parameter with the name you use for store the authenticated user on the request. For example:

sessions.resolver.ts

@Query(() => [Session], { name: 'sessions', nullable: 'items' })
async findMany(
  ...,
  @AuthUser('user') authUser: User
) {
  return await this.sessionsService.findMany(..., authUser);
}

This is pretty much usefull for when use findMany queries where the set of data is returned along the count number.

Owner

This util function permits modify the TypeORM FindOptionsWhere for adding checks of ownership with an authenticated user and defined roles. For example if we want to return only the sessions that are owned by the authenticated user:

sessions.resolver.ts

@Query(() => Session, { name: 'session', nullable: true })
async findOne(
  @Args('id', { type: () => GraphQLUUID }) id: string,
  @SelectionSet() selection: SelectionInput,
  @AuthUser() authUser: User
) {
  return await this.sessionsService.findOne(id, selection, authUser);
}

@Query(() => [Session], { name: 'sessions', nullable: 'items' })
async findMany(
  @Args('where', { type: () => [SessionWhereInput], nullable: true }, TypeORMWhereTransform<Session>)
  where: FindOptionsWhere<Session>,
  @Args('order', { type: () => [SessionOrderInput], nullable: true }, TypeORMOrderTransform<Session>)
  order: FindOptionsOrder<Session>,
  @Args('pagination', { nullable: true }) pagination: PaginationInput,
  @SelectionSet() selection: SelectionInput,
  @AuthUser() authUser: User
) {
  return await this.sessionsService.findMany(where, order, pagination, selection, authUser);
}

Note: you previously have to add user to the context request with your own authentication guard.

sessions.service.ts

async findOne(id: string, selection: SelectionInput, authUser: User) {
  return await this.sessionsRepository.findOne({
    relations: selection?.getTypeORMRelations(),
    where: Owner({ id: id }, 'user.id', authUser, [Role.ADMIN])
  });
}

async findMany(
  where: FindOptionsWhere<Session>,
  order: FindOptionsOrder<Session>,
  pagination: PaginationInput,
  selection: SelectionInput,
  authUser: User
) {
  return await this.sessionsRepository.find({
    relations: selection?.getTypeORMRelations(),
    where: Owner(where, 'user.id', authUser, [Role.ADMIN]),
    order: order,
    skip: pagination ? (pagination.page - 1) * pagination.count : null,
    take: pagination ? pagination.count : null
  });
}

Here users with role "admin" can bypass the ownership check so the where option is not modified for them. You can put all the roles you want to bypass in that parameter.

There's also a fifth parameter that allow to modify the acceded authUser idField and roleField to be different as default ones ('id' and 'role').

InputTypes Reference

StringWhereInput

input StringWhereInput {
  eq: String
  ne: String
  like: String
  ilike: String
  in: [String!]
  any: [String!]
  and: [StringWhereInput!]
  or: [StringWhereInput!]
  not: StringWhereInput
}

IntWhereInput

input IntWhereInput {
  eq: Int
  ne: Int
  gt: Int
  gte: Int
  lt: Int
  lte: Int
  in: [Int!]
  any: [Int!]
  between: [Int!]
  and: [IntWhereInput!]
  or: [IntWhereInput!]
  not: IntWhereInput
}

FloatWhereInput

input FloatWhereInput {
  eq: Float
  ne: Float
  gt: Float
  gte: Float
  lt: Float
  lte: Float
  in: [Float!]
  any: [Float!]
  between: [Float!]
  and: [FloatWhereInput!]
  or: [FloatWhereInput!]
  not: FloatWhereInput
}

DateTimeWhereInput

input DateTimeWhereInput {
  eq: DateTime
  ne: DateTime
  gt: DateTime
  gte: DateTime
  lt: DateTime
  lte: DateTime
  in: [DateTime!]
  any: [DateTime!]
  between: [DateTime!]
  and: [DateTimeWhereInput!]
  or: [DateTimeWhereInput!]
  not: DateTimeWhereInput
}

BooleanWhereInput

input BooleanWhereInput {
  eq: Boolean
  ne: Boolean
  and: [BooleanWhereInput!]
  or: [BooleanWhereInput!]
  not: BooleanWhereInput
}

PaginationInput

input PaginationInput {
  count: Int!
  page: Int!
}

OrderDirection

"""
Defines the order direction.
"""
enum OrderDirection {
  """
  Ascending order.
  """
  ASC

  """
  Descending order.
  """
  DESC
}

Stay in touch

Me

Nest

License

Nest and this package are MIT licensed.