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
}
}
}
Pingback: [Startup MVP recipes #13] Quick glance: Inheritance of TypeOrm’s Entity and GraphQL’s Object - James Zhang