Last active
December 5, 2022 07:28
-
-
Save mattpetters/7c94b775011d3aabfc73e2b6f9837e74 to your computer and use it in GitHub Desktop.
TypeORM + TypeScript + Dataloader + Relay Spec (connections, pagination)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
... | |
// Create the GraphQL server | |
const server = new ApolloServer({ | |
schema, | |
context: ({ req, res }): AppContext => { | |
return { | |
req, | |
res, | |
loaders: { | |
reviewLoader: reviewsByProductLoader() | |
} | |
} | |
} | |
... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Review } from '../entity/Review'; | |
import { EdgeType } from './generics/EdgeType'; | |
@EdgeType(Review) | |
export class ReviewEdge {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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