Skip to content

[Startup MVP recipes #6] GraphQL Resolver inheritance and a CRUD base resolver with generics

Background

For your background information, we have our own defined auth guard named MagentoAuth (that interacts with Magento and issue our own tokens) and we have a sellerId which is just the user id. We self defined @CurrentSellerId() to get the sellerId for GQL from execution context.

We found that for basic entities and GQL CRUD endpoints for them are quite repetitive and therefore we want to define some base resolvers and new simple entity can directly inherit from them to get the necessary queries or mutations to get started with.

In our nest.js + graphql + typeorm setup we just directly inject the db resource repository to the resolver for now.

Removed results interface

First previously we haved defined a removed result interface that can reflect the items removed + affected rows count

import { Type } from '@nestjs/common';
import { Field, Int, ObjectType } from '@nestjs/graphql';

export interface IRemoveResults<T> {
  returning: T[];
  affectedRows: number;
}

export function RemoveResults<T>(ItemType: Type<T>): any {
  @ObjectType({ isAbstract: true })
  abstract class RemoveResultsClass implements IRemoveResults<T> {
    @Field(() => [ItemType])
    returning: T[];

    @Field(() => Int)
    affectedRows: number;
  }

  return RemoveResultsClass;
}

Define the base resolver

Then here is the code snippet for the Base Resolver:

import { Type, UseGuards } from '@nestjs/common';
import {
  Args,
  Int,
  Mutation,
  ObjectType,
  Query,
  Resolver,
} from '@nestjs/graphql';
import * as pluralize from 'pluralize';
import { BaseEntity, DeepPartial, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { CurrentSellerId } from '../shared/currentSellerId.decorator';
import { MagentoGqlAuthGuard } from '../shared/magentoGqlAuthGuard';
import {
  IRemoveResults,
  RemoveResults,
} from '../utils/sharedDtos/removeResult';
import { convertFirstLevelSnakeToCamelCase } from '../utils/sharedDtos/utils';

export interface IMagentoAuth {
  id: number;
  sellerId: number;
}

const PREFIX = '_gen_';

export function BaseMagentoAuthResolver<
  T extends BaseEntity & IMagentoAuth,
  K extends DeepPartial<T>,
  L extends QueryDeepPartialEntity<T>,
>(
  classRef: Type<T>,
  CreateClassRefInput: Type<K>, // pass in custom create DTO
  UpdateClassRefInput: Type<L>, // pass in custom update DTO
): any {
  @ObjectType(`Remove${classRef.name}Output`)
  class RemoveClassRefOutput extends RemoveResults(classRef) {}

  @Resolver({ isAbstract: true })
  @UseGuards(MagentoGqlAuthGuard)
  abstract class BaseResolverHost {
    private repository: Repository<T>;
		// inject typeorm's repository through constructor
    constructor(repository: Repository<T>) {
      this.repository = repository;
    }

    @Mutation(() => classRef, {
      name: `${PREFIX}create${classRef.name}`,
    })
    async create(
      @CurrentSellerId() sellerId: number,
      @Args(`create${classRef.name}Input`, { type: () => CreateClassRefInput })
      createClassRefInput: K,
    ) {
      return this.repository.save({ ...createClassRefInput, sellerId });
    }

    @Mutation(() => classRef, {
      name: `${PREFIX}update${classRef.name}`,
      nullable: true,
    })
    async update(
      @CurrentSellerId() sellerId: number,
      @Args(`update${classRef.name}Input`, { type: () => UpdateClassRefInput })
      updateClassRefInput: L,
    ) {
      const updateResult = await this.repository
        .createQueryBuilder()
        .update(classRef)
        .where({
          id: updateClassRefInput.id,
          sellerId,
        })
        .set(updateClassRefInput)
        .returning('*')
        .execute();
      return convertFirstLevelSnakeToCamelCase(updateResult.raw[0]);
    }

    @Query(() => [classRef], { name: `${PREFIX}${pluralize(classRef.name)}` })
    async findAll(@CurrentSellerId() sellerId: number): Promise<T[]> {
      return this.repository.find({
        where: {
          sellerId,
        },
      });
    }

    @Query(() => classRef, {
      name: `${PREFIX}${classRef.name}`,
      nullable: true,
    })
    async findOne(@CurrentSellerId() sellerId: number, id: number): Promise<T> {
      return this.repository.findOne({
        where: {
          id,
          sellerId,
        },
      });
    }

    @Mutation(() => RemoveClassRefOutput, {
      name: `${PREFIX}remove${pluralize(classRef.name)}`,
    })
    async removeOne(
      @CurrentSellerId() sellerId: number,
      @Args('id', { type: () => Int }) id: number,
    ) {
      const rawResult = await this.repository
        .createQueryBuilder()
        .delete()
        .where({
          id,
          sellerId,
        })
        .returning('*')
        .execute();
      const returnings = rawResult.raw.map((row: any) => {
        return row ? _.mapKeys(row, (_v: any, k: any) => _.camelCase(k)) : null;
      });
      const result: IRemoveResults<T> = {
        returning: returnings as T[],
        affectedRows: rawResult.affected,
      };
      return result;
    }
  }
  return BaseResolverHost;
}

Usage and the actual inheritance

The entity with just one example field:

// sample-entity.entity.ts

@ObjectType()
@Entity()
export class SampleEntity extends BaseEntity {
  @Field(() => ID, { description: 'Auto-increment int ID' })
  @PrimaryGeneratedColumn()
  id!: number;

  @CreateDateColumn({
    type: 'timestamptz',
    update: false,
  })
  createdAt!: Date;

  @UpdateDateColumn({
    type: 'timestamptz',
  })
  updatedAt!: Date;

  @Field({ description: 'External sellerId from Seller Portal' })
  @Index()
  @Column({ nullable: false })
  @IsInt()
  @Min(0)
  sellerId!: number;

  @Field(() => Int, {
    nullable: true,
    description: 'Example field (placeholder)',
  })
  @Column({ nullable: true })
  exampleField: number;
}

We still need to define the create and update DTO:

// create-sample-entity.input.ts

@InputType()
export class CreateSampleEntityInput {
  @Field(() => Int, { description: 'Example field (placeholder)' })
  exampleField: number;
}
// update-sample-entity.input.ts

@InputType()
export class UpdateSampleEntityInput extends PartialType(
  CreateSampleEntityInput,
) {
  @Field(() => Int)
  id: number;
}

Then let’s connect everything and inject params and inherit from the base resolver:

// sample-entities.resolver.ts

@Resolver(() => SampleEntity)
export class SampleEntitiesResolver extends BaseMagentoAuthResolver(
  SampleEntity,
  CreateSampleEntityInput,
  UpdateSampleEntityInput,
) {
  constructor(
    @InjectRepository(SampleEntity)
    sampleEntityRepo: Repository<SampleEntity>,
  ) {
    super(sampleEntityRepo);
  }
}

So very easily we create the DTOs then pass them in and construct the resolver with the repository and then that’s it.

Generated Queries/Mutations:

Create

mutation Mutation($createSampleEntityInput: CreateSampleEntityInput!) {
  _gen_createSampleEntity(createSampleEntityInput: $createSampleEntityInput) {
    exampleField
    id
    sellerId
  }
}

Read

query Query {
  _gen_SampleEntities {
    exampleField
    id
    sellerId
  }
}

query Query {
  _gen_SampleEntity {
    exampleField
    id
    sellerId
  }
}

Update

mutation _gen_updateSampleEntity($updateSampleEntityInput: UpdateSampleEntityInput!) {
  _gen_updateSampleEntity(updateSampleEntityInput: $updateSampleEntityInput) {
    exampleField
    id
    sellerId
  }
}

Delete

mutation _gen_removeSampleEntities($genRemoveSampleEntitiesId: Int!) {
  _gen_removeSampleEntities(id: $genRemoveSampleEntitiesId) {
    affectedRows
    returning {
      exampleField
      id
      sellerId
    }
  }
}

1 thought on “[Startup MVP recipes #6] GraphQL Resolver inheritance and a CRUD base resolver with generics”

  1. Pingback: [Startup MVP recipes #13] Quick glance: Inheritance of TypeOrm’s Entity and GraphQL’s Object - James Zhang

Leave a Reply

Your email address will not be published. Required fields are marked *