Stoyko Tolev
Stoyko Tolev

Reputation: 61

How to return the entity with its relations after saving it?

I am building a storage application, with GraphQL as the backend, using Typegraphql and TypeORM.

The categories need to be added separately and then when adding a product, you choose from a dropdown one of the available categories. This in turn passes the categoryId to the product in a one-to-many/many-to-one relationship.

Here is my Category entity:

import {
  Entity,
  PrimaryColumn,
  Column,
  BaseEntity,
  Generated,
  OneToMany
} from 'typeorm';
import Product from './Product';

@ObjectType()
@Entity('categories')
export default class Category extends BaseEntity {
  @Field()
  @PrimaryColumn()
  @Generated('uuid')
  categoryId: string;

  @Field()
  @Column()
  categoryName: string;

  @OneToMany(() => Product, (product: Product) => product.category)
  products: Product[];
}

and here is my Product entity

import {
  Entity,
  PrimaryColumn,
  Column,
  BaseEntity,
  Generated,
  ManyToOne,
  JoinColumn
} from 'typeorm';
import Category from './Category';

@ObjectType()
@Entity('products')
export default class Product extends BaseEntity {
  @Field()
  @PrimaryColumn()
  @Generated('uuid')
  productID: string;

  @Field()
  @Column()
  productName: string;

  @Field(() => Category)
  @ManyToOne(() => Category, (category: Category) => category.products, {
    cascade: true,
    lazy: true
  })
  @JoinColumn()
  category: Category;

  @Field()
  @Column()
  productQuantity: number;

  @Field()
  @Column({ type: 'decimal', precision: 2 })
  productPrice: number;

  @Field()
  @Column({ type: 'decimal', precision: 2 })
  productPriceRA: number;

  @Field()
  @Column({ type: 'decimal', precision: 2 })
  productPriceKK: number;

  @Field()
  @Column('varchar', { length: 255 })
  productSupplier: string;

  @Field()
  @Column('varchar', { length: 255 })
  productOrderLink: string;

  @Field()
  @Column('longtext')
  productImage: string;
}

For the save mutation, I've created an Input type as well:

export default class ProductInput implements Partial<Product> {
    @Field()
    productName: string;

    @Field(() => String)
    category: Category;

    @Field()
    productQuantity: number;

    @Field()
    productPrice: number;

    @Field()
    productPriceRA: number;

    @Field()
    productPriceKK: number;

    @Field()
    productSupplier: string;

    @Field()
    productOrderLink: string;

    @Field()
    productImage: string;
}

The relations work, as I am able to query the products, along with their category data with the following query:

{
  getProducts {
    productID
    productName
    category {
       categoryId
       categoryName
    }
  }
}

However, when saving a product it always returns "message": "Cannot return null for non-nullable field Category.categoryName."

This is the Mutation's code in the Resolver:

@Mutation(() => Product, { description: 'Add new product' })
  async addProduct(
    @Arg('product') productInput: ProductInput
  ): Promise<Product | any> {
    try {
      const product = await Product.create(productInput).save();

      console.log('product: ', product);
      return product;
    } catch (error) {
      return error;
    }
  }

I've been trying different things, however nothing seems to work and I am wondering if it's even possible to directly return the entity with its relations. If it's not, the other option I can think of is to return true/false based on the result and re-query all of the data. But this seems very inefficient and I am actively trying to avoid going this route.

Any help will be much appreciated.

Upvotes: 3

Views: 7557

Answers (2)

umutyerebakmaz
umutyerebakmaz

Reputation: 1037

GraphQL uses an notation to recognize data. You can see it as __typename object property. Of course, this must be turned on in the GraphQL server configuration. If you see it, it's already clear. You can reach the correct result without refetching the relation changes in the cached data on the client side with a trick like this. For example, let's say we have updated the Product with category. In the data to return from the update mutation, it is sufficient to return only the id of the relation.

For this to work, category and product must be cached separately on the client beforehand.

for example:

mutation UpdateProduct($product: UpdateProductInput!) {
    updateProduct(product: $product) {
        id
        title
        category {
            id
        }
    }
}

You can also write in writeFragment, which is a separate method, which is the most stingy, but it can make your job difficult in nested data.

export class ProductFragmentService {
    constructor(private apollo: Apollo) {}

    updateProduct(product: Product): void {
        const client = this.apollo.client;
        client.writeFragment({
            id: `Product:${product.id}`,
            fragment: gql`
                fragment UpdateProductCategoryFragment on Product {
                    __typename
                    id
                    title
                    category {
                        id
                    }
                }
            `,
            data: {
                __typename: 'Product',
                ...product,
            },
        });
    }
}
 

If you want all the fields belonging to category, you need to send them to resolver and return as a response from there. Otherwise, yes, it gives a warning that I could not find the name property.

The more profitable way of doing it is to send this data to the resolver with the input, as I wrote above, and return to the client as a response from the server.

If you still have to make another SQL request, it is necessary to call the same id after registration.

@Authorized()
@Mutation(() => Product, { description: 'Add new product' })
  async addProduct(
    @Arg('product') productInput: ProductInput
  ): Promise<Product> {
      await this.productRepo.save(productInput);
      return await this.productRepo.findOne({ where: { id: productInfo.id } });
  }

that's all :)

Upvotes: 0

Stoyko Tolev
Stoyko Tolev

Reputation: 61

After some more research and I decided to go with the following approach:

try {
      const { productID } = await Product.create(productInput).save();

      return await Product.findOne(productID);
    } catch (error) {
      return error;
    }

This allows me to directly return the product, based on the productID after it's saved in the database and properly returns the object with it's relationship.

Upvotes: 3

Related Questions