I'm trying to define generic type definitions for building connections and edges in GraphQL. I'm keeping with the Relay spec, except that I'm also including the conventional nodes
connection property for convenience. The TypeGraphQL docs show how to do something very similar, but I'm getting the following error when I try to run the server:
Cannot determine GraphQL output type for 'nodes' of 'Connection' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?
Typically, this shows up when the @ObjectType
decorator is missing from a class definition which is then used as a field type somewhere else; however, I've confirmed that the types I'm passing have definitely been defined with this decorator, and I've also tried with a few different types from my schema. The docs demonstrate using a generic as a field definition, so that doesn't seem to be the issue either.
My type definitions are as follows:
interface RawEdge<NodeType> {
node: NodeType
}
interface Edge<NodeType> extends RawEdge<NodeType> {
cursor: string
}
function Edge<NodeType>(NodeClass: ClassType<NodeType>) {
@ObjectType({ isAbstract: true })
abstract class Edge {
constructor(identifier: string, node: NodeType) {
this.cursor = Buffer.from(identifier).toString('base64')
this.node = node
}
@Field()
cursor: string
@Field(type => NodeClass)
node: NodeType
}
return Edge
}
interface Connection<NodeType, EdgeType extends Edge<NodeType>> {
totalCount: number
edges: EdgeType[]
nodes: NodeType[]
pageInfo: PageInfo
}
function Connection<NodeType, EdgeType extends Edge<NodeType>>(
NodeClass: ClassType<NodeType>,
EdgeClass: ClassType<Edge<NodeType>>
) {
@ObjectType({ isAbstract: true })
abstract class Connection {
constructor(edges: EdgeType[], page: PageQuery) {
this.totalCount = edges.length
this.edges = edgesToReturn<EdgeType>(edges, page)
this.nodes = this.edges.map(edge => edge.node)
this.pageInfo = pageInfo(this, edges)
}
@Field(type => Int)
totalCount: number
@Field(type => [EdgeClass])
edges: EdgeType[]
@Field(type => [NodeClass])
nodes: NodeType[]
@Field()
pageInfo: PageInfo
}
return Connection
}
EDIT: The following workaround resolves the type issue, which proves that the types being passed in have the proper decorators in their definition. However, this is extremely clunky, so I would like to avoid having to use it if possible.
function Edge<NodeType>(NodeClass: ClassType<NodeType>) {
@ObjectType({ isAbstract: true })
abstract class Edge {
constructor(identifier: string, node: NodeType) {
this.cursor = Buffer.from(identifier).toString('base64')
this.node = node
}
@Field()
cursor: string
// @Field decorator removed
node: NodeType
}
return Edge
}
function Connection<NodeType, EdgeType extends Edge<NodeType>>(
NodeClass: ClassType<NodeType>,
EdgeClass: ClassType<Edge<NodeType>>
) {
@ObjectType({ isAbstract: true })
abstract class Connection {
constructor(edges: EdgeType[], page: PageQuery) {
this.totalCount = edges.length
this.edges = edgesToReturn<EdgeType>(edges, page)
this.nodes = this.edges.map(edge => edge.node)
this.pageInfo = pageInfo(this, edges)
}
@Field(type => Int)
totalCount: number
@Field(type => [EdgeClass])
edges: EdgeType[]
// @Field decorator removed
nodes: NodeType[]
@Field()
pageInfo: PageInfo
}
return Connection
}
type RawDepartmentProductEdge = RawEdge<Product>
@ObjectType()
class DepartmentProductEdge extends Edge(Product) {
// Define the field type here instead of in the generic
@Field(type => Product)
node: Product
}
@ObjectType()
class DepartmentProductConnection extends Connection(Product, DepartmentProductEdge) {
// Define the field type here instead of in the generic
@Field(type => [Product])
nodes: Product[]
}
Context, for anyone who's curious:
The purpose of all this is to generate connections using something like this...
type RawDepartmentProductEdge = RawEdge<Product>
@ObjectType()
class DepartmentProductEdge extends Edge(Product) {}
@ObjectType()
class DepartmentProductConnection extends Connection(Product, DepartmentProductEdge) {}
...and then populate them like this...
const products = [] // Retrieve the products here
const edges = products.map(node => new DepartmentProductEdge(node.id, node)
const connection = new DepartmentProductConnection(edges, pagination)
...so that everything is as DRY as possible, but I can still add meta to the edges as needed.