Reputation: 61
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
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
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