2

I am trying to make a Graphql server with Mikro-orm and having a problem.

Here's my Product.ts entity;

    import {
      Entity,
      IdentifiedReference,
      ManyToOne,
      PrimaryKey,
      Property
    } from '@mikro-orm/core'
    import { Field, Float, ObjectType } from 'type-graphql'
    import { ProductArguments } from '../util/types'
    import { Category } from './Category'
    
    @Entity()
    @ObjectType()
    export class Product {
      constructor({ title, price, description }: ProductArguments) {
        this.title = title
        this.price = price
        this.description = description
      }
    
      @Field()
      @PrimaryKey()
      id!: number
    
      @Field()
      @Property()
      title!: string
    
      @Field()
      @Property()
      imageUrl!: string
    
      @Field(() => Float)
      @Property({ type: 'decimal' })
      price!: number
    
      @Field(() => String)
      @Property({ type: 'text' })
      description!: string
    
      @Field(() => Category)
      @ManyToOne(() => Category)
      category!: IdentifiedReference<Category>
    
      @Field()
      @Property()
      createdAt: Date = new Date()
    
      @Field()
      @Property({ onUpdate: () => new Date() })
      updatedAt: Date = new Date()
    }

Here's my Category.ts entity

  import {
    Cascade,
      Collection,
      Entity,
      LoadStrategy,
      OneToMany,
      PrimaryKey,
      Property
    } from '@mikro-orm/core'
    import { Field, ObjectType } from 'type-graphql'
    import { Product } from './Product'
    
    @Entity()
    @ObjectType()
    export class Category {
      constructor(title: string) {
        this.title = title
      }
    
      @Field()
      @PrimaryKey()
      id!: number
    
      @Field()
      @Property()
      title!: string
    
      @Field(() => [Product])
      @OneToMany({
        entity: () => Product,
        mappedBy: (product) => product.category,
        cascade: [Cascade.REMOVE],
        strategy: LoadStrategy.JOINED
      })
      products = new Collection<Product>(this)
    
      @Field()
      @Property()
      createdAt: Date = new Date()
    
      @Field()
      @Property({ onUpdate: () => new Date() })
      updatedAt: Date = new Date()
    }

Basically, I have a one-to-many relationship with category and product. Each product should belong to a category and a category can have multiple products.

Now in my resolvers, I am trying to get all the categories with the following.

   const categories = await em
      .createQueryBuilder(Category)
      .select('*')
      .getResult()
    return em.populate(categories, ['products'])

This works perfectly fine. No errors. But when I try to get the all products with their categories like the following:

   const products = await em
      .createQueryBuilder(Product)
      .select('*')
      .getResult()
    return em.populate(products, ['category'])

Or with left join

   return em
      .createQueryBuilder(Product, 'p')
      .select('*')
      .leftJoinAndSelect('p.category', 'c')
      .getResultList()

I got the following error Here's the generated query.

select "p".*, "c"."id" as "c__id", "c"."title" as "c__title", "c"."created_at" as "c__created_at", "c"."updated_at" as "c__updated_at" from "product" as "p" left join "category" as "c" on "p"."category_id" = "c"."id"```

I also tried to run this query in the database and I got the actual result that I wanted. But in graphql, I get the following error. What am I doing wrong here?

  {
      "message": "Cannot return null for non-nullable field Category.title.",
      "locations": [
        {
          "line": 11,
          "column": 7
        }
      ],
      "path": [
        "products",
        0,
        "category",
        "title"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Cannot return null for non-nullable field Category.title.",
xadm
  • 8,219
  • 3
  • 14
  • 25
Pranta
  • 2,928
  • 7
  • 24
  • 37
  • What does that even mean? – Pranta Nov 25 '21 at 03:04
  • log data from DB before return ... category prop should be an object with title field, it's empty/null/undefined ... are you following some tutorial? graphql shouldn't overfetch, product resolver should return only product fields, "missing" category value[if requested] should call separate field resolver to read related category data (parent/root/1st arg - already resolved product object - contains category_id, can be used for categories DB read) ... of course, you can overfetch [to optimize DB reads] but at cost of limited scalability – xadm Nov 25 '21 at 12:17

1 Answers1

0

For anyone who is having the same problem as me, look at this lines

  @Field(() => Category)
  @ManyToOne(() => Category)
  category!: IdentifiedReference<Category>

I shouldn't have included IdentifiedReference without understanding it properly first. From the docs,

IdentifiedReference is an intersection type that adds primary key property to the Reference interface. It allows to get the primary key from the Reference instance directly.

Basically what happened here is that I was getting a reference with the primary key of the product table. But I was accessing the title property from it. There is a getProperty function to get the property/database-field from it thouth. But replacing the code like the following solved my problem.

  @Field(() => Category)
  @ManyToOne(() => Category)
  category!: Category

You can follow our github discussion here.

Pranta
  • 2,928
  • 7
  • 24
  • 37