Skip to content

Instantly share code, notes, and snippets.

@mattpetters
Last active December 5, 2022 07:28
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattpetters/7c94b775011d3aabfc73e2b6f9837e74 to your computer and use it in GitHub Desktop.
Save mattpetters/7c94b775011d3aabfc73e2b6f9837e74 to your computer and use it in GitHub Desktop.
TypeORM + TypeScript + Dataloader + Relay Spec (connections, pagination)
...
// Create the GraphQL server
const server = new ApolloServer({
schema,
context: ({ req, res }): AppContext => {
return {
req,
res,
loaders: {
reviewLoader: reviewsByProductLoader()
}
}
}
...
import { Connection, Edge } from 'graphql-relay'
import { ClassType, Field, ObjectType } from 'type-graphql'
import { PageInfo } from '../PageInfo'
export function ConnectionType<V>(EdgeType: ClassType<V>) {
// `isAbstract` decorator option is mandatory to prevent registering in schema
@ObjectType({ isAbstract: true, description: 'A connection to a list of items.'})
abstract class ConnectionClass implements Connection<V>{
// here we use the runtime argument
@Field(() => [EdgeType], {
description: 'A list of edges.',
nullable: 'itemsAndList'
})
readonly edges!: Array<Edge<V>>
@Field({ description: 'Information to aid in pagination.' })
pageInfo!: PageInfo;
}
return ConnectionClass;
}
import { ConnectionCursor, Edge } from 'graphql-relay'
import { ClassType, Field, ObjectType } from 'type-graphql'
export function EdgeType<V extends ClassType, T extends ClassType>(
NodeType: V
) {
return (target: T): ClassType => {
@ObjectType(target.name, { description: 'An edge in a connection.' })
class EdgeType extends target implements Edge<V> {
@Field(() => NodeType, {
description: 'The item at the end of the edge.'
})
readonly node!: V
@Field(() => String, { description: 'A cursor for use in pagination.' })
readonly cursor!: ConnectionCursor
}
return EdgeType
}
}
import { Field, ID, InterfaceType } from 'type-graphql'
import { BaseEntity } from 'typeorm'
@InterfaceType('Node', { description: 'An object with a global ID.' })
export abstract class NodeInterface extends BaseEntity {
@Field(() => ID, {
name: 'id',
description: 'The global ID of the object.'
})
readonly globalId!: string
}
import { ConnectionCursor, PageInfo as RelayPageInfo } from 'graphql-relay'
import { Field, ObjectType } from 'type-graphql'
@ObjectType({ description: 'Information about pagination in a connection.' })
export class PageInfo implements RelayPageInfo {
@Field(() => String, {
nullable: true,
description: 'When paginating backwards, the cursor to continue.'
})
startCursor?: ConnectionCursor | null
@Field(() => String, {
nullable: true,
description: 'When paginating forwards, the cursor to continue.'
})
endCursor?: ConnectionCursor | null
@Field(() => Boolean, {
nullable: true,
description: 'When paginating backwards, are there more items?'
})
hasPreviousPage?: boolean | null
@Field(() => Boolean, {
nullable: true,
description: 'When paginating forwards, are there more items?'
})
hasNextPage?: boolean | null
}
import { connectionFromArray } from "graphql-relay"
@FieldResolver()
async reviews(@Root() product: Product, @Args() args: ConnectionArgs, @Ctx() {loaders}: TPContext){
const reviewRepo = getRepository(Review);
// dataload it using a reviewsByProductId loader
const dataload = await loaders.reviewLoader.load(product._id.toString());
// handle the no reviews case
if (dataload) {
//returns spec compliant connection
const connection = connectionFromArray(dataload, args);
return {
...connection,
totalReviews: 0, //metadata for connection here, WIP compute this efficiently
averageScore: 0
};
}
return null;
}
@ObjectType({implements: NodeInterface})
@Entity()
export class Product extends NodeInterface {
__typename?: "Product";
@Field(() => ID)
@PrimaryGeneratedColumn()
_id: string;
@Field(() => ReviewConnection, {nullable: true})
@OneToMany(() => Review, (review) => review.product)
reviews?: Connection<Review>; // Depending on Interface here not derived type was a key for me to get things working
/** Product title */
@Field(() => String, { nullable: false })
@Column()
title: string;
}
import { Field, Float, Int, ObjectType } from "type-graphql";
import { ConnectionType } from "./generics/ConnectionType";
import { ReviewEdge } from "./ReviewEdge";
@ObjectType()
export class ReviewConnection extends ConnectionType(ReviewEdge) {
@Field(()=> Float)
totalReviews: number;
@Field(()=>Int)
averageScore: number;
}
import { Review } from '../entity/Review';
import { EdgeType } from './generics/EdgeType';
@EdgeType(Review)
export class ReviewEdge {}
import * as DataLoader from "dataloader";
import { groupBy, map } from "ramda";
import { getConnection } from "typeorm";
import { Review } from "../entity/Review";
export function reviewsByProductLoader(){
return new DataLoader(reviewsByProductIds);
}
const reviewsByProductIds = async (productIds:string[]) => {
const result = await getConnection()
.createQueryBuilder()
.select("*")
.from(Review, "review")
.where("\"product_id\" IN (:...productIds)", { productIds })
.getRawMany()
const reviews = result;
const groupedById = groupBy((review: Review) => review.productId, reviews)
return map(productId => groupedById[productId], productIds);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment