Search code examples
dependency-injectionfactorytypeormtypegraphql

How to implement Resolver-Inheritance with Type-GraphQL, TypeORM, and dependency injection


I am trying to extend upon the Type-GraphQL provided example of resolvers-inheritance except replace the static data with a TypeORM repository.

Here is how the PersonResolver extends the ResourceResolver and how it passes the persons array as the second argument of the ResourceResolver constructor.

const persons: Person[] = [
  {
    id: 1,
    name: "Person 1",
    age: 23,
    role: PersonRole.Normal,
  },
  {
    id: 2,
    name: "Person 2",
    age: 48,
    role: PersonRole.Admin,
  },
];

@Resolver()
export class PersonResolver extends ResourceResolver(Person, persons) {
   ...
}

Inside the ResourceResolver

export function ResourceResolver<TResource extends Resource>(
  ResourceCls: ClassType<TResource>,
  resources: TResource[],
) {
  const resourceName = ResourceCls.name.toLocaleLowerCase();

  // `isAbstract` decorator option is mandatory to prevent multiple registering in schema
  @Resolver(_of => ResourceCls, { isAbstract: true })
  @Service()
  abstract class ResourceResolverClass {
    protected resourceService: ResourceService<TResource>;

    constructor(factory: ResourceServiceFactory) {
      this.resourceService = factory.create(resources);
    }
...
}

And in the ResourceServiceFactory

@Service()
export class ResourceServiceFactory {
  create<TResource extends Resource>(resources?: TResource[]) {
    return new ResourceService(resources);
  }
}

export class ResourceService<TResource extends Resource> {
  constructor(protected resources: TResource[] = []) {}

    getOne(id: number): TResource | undefined {
    return this.resources.find(res => res.id === id);
}

I would like to know the best way to implement the ResourceResolver but instead of static data I would like to pass a repository from TypeORM.

Here is the original example - https://github.com/MichalLytek/type-graphql/tree/master/examples/resolvers-inheritance.

Any help or advice is greatly appreciated.


Solution

  • I believe you'd have to do something funky like this in your resolver functions:

    function createBaseResolver<T extends BaseEntity>(suffix: string, objectTypeCls: T) {
        @Resolver({ isAbstract: true })
        abstract class BaseResolver {  
          @Query(type => [objectTypeCls], { name: `getAll${suffix}` })
          async getA(@Arg("id", type => Int) id: number): Promise<T> {
              let beCastedObj = (<typeof BaseEntity> objectTypeCls.constructor); // https://github.com/Microsoft/TypeScript/issues/5677
              return beCastedObj.findOne({ where: { id:id } }) as Promise<T>;
          }
        }
      
        return BaseResolver;
      }
    

    in another file...

    const PersonBaseResolver = createBaseResolver("person", Person);
    
    @Resolver(of => Person)
    export class PersonResolver extends PersonBaseResolver {
      // ...
    }
    

    EDIT: OP asked if he thought this is a good pattern in the comments. I'm just starting to learn this, so don't take my word as gospel. However. I encountered trouble when taking the next step: defining custom arguments. If you use custom classes for the @Args() (recommended as it can auto-validate), then typescript/typegraphql NEEDS that info at compile time. You can keep that parent's resolvers as-is if these args will not change across any of the children, but if they change you need a way to pass in custom arguments.

    I accomplished this by moving the resolver decorators to the children.

    Example Parent:

    abstract class BaseUserCreatedEntityResolver {
    
        async get(args: any, ctx: any): Promise<T> {
          this.checkForLogin(ctx);
          let beCastedObj = (<typeof UserCreatedEntity>objectTypeCls.constructor);
          args = Object.assign(args, { userCreator: ctx.req.session.userId })
          let a =  beCastedObj.findOne({ where: args }) as any;
          return a;
        }
    
        async getAll(args: any, ctx: any): Promise<T> {
          this.checkForLogin(ctx);
          let beCastedObj = (<typeof UserCreatedEntity>objectTypeCls.constructor);
          args = Object.assign(args, { userCreator: ctx.req.session.userId });
          beCastedObj.create(args);
          return beCastedObj.find({ where: args }) as any;
        }
    
        async add(args:any, ctx: any): Promise<T> {
          this.checkForLogin(ctx);
          let beCastedObj = (<typeof UserCreatedEntity>objectTypeCls.constructor);
          args = Object.assign(args, { userCreator: ctx.req.session.userId });
          let entity = await beCastedObj.create(args)[0];
          await entity.save();
          return entity as any;
        }
    
        async delete(args:any, ctx: any): Promise<T> {
          this.checkForLogin(ctx);
          let entity = await this.get(args,ctx);
          await entity.remove();
          return new Promise(()=>true);
        }
    
        async update(args:any, ctx: any): Promise<T> {
          this.checkForLogin(ctx);
          let entity = await this.get(args,ctx);
          delete args['userCreator'];// this should've been filtered out in child param definition, but adding it here just in case
    
          Object.assign(entity,args);
          await entity.save();
          return entity;
        }
    
        checkForLogin(ctx:any){
          if(!ctx.req.session.userId) throw new Error("User not logged in");
        }
    
      }
    

    Example Child:

    @ArgsType()
    class GetAllArgs {
      @Field()
      date:Date;
    }
    
    //...
    
    @Query(() => Entity)
    async getAllEntitiesName(@Args() args :GetAllArgs, @Ctx() ctx: any) {
      return super.get(args,ctx);
    }
    

    I quite like this paradigm. And if I ever have a function that doesn't change across all the children resolvers, I will create that function and decorate it in the parent. For that reason, I am leaving the parent as a customizable function-class, instead of just a basic class.