vendure-ecommerce / vendure

The commerce platform with customization in its DNA.
https://www.vendure.io
Other
5.6k stars 992 forks source link

V2.2.0 @Transaction() resolver will lead QueryRunnerAlreadyReleasedError: Query runner already released. Cannot run queries anymore. #2796

Open tianyingchun opened 5 months ago

tianyingchun commented 5 months ago

Describe the bug

when we use @Transaction() resolver

it will lead QueryRunnerAlreadyReleasedError: Query runner already released. Cannot run queries anymore.


@Resolver()
export class ProductRecommendationAdminResolver {
  constructor(
    private productRecommendationService: ProductRecommendationService
  ) {}

  @Transaction()
  @Mutation()
  @Allow(Permission.UpdateCatalog)
  async updateCrossSellingProducts(
    @Ctx() ctx: RequestContext,
    @Args() args: { productId: ID; productIds: [ID] }
  ): Promise<boolean> {
    const recommendations: ProductRecommendation[] =
      await this.productRecommendationService.findAll(ctx, {
        where: {
          productId: args.productId,
          type: RecommendationType.CROSSSELL,
        },
      });

    const recommendationsIds = recommendations.map((r) => r.recommendationId);

    const toDelete = recommendations
      .filter((r) => !args.productIds.includes(r.recommendationId))
      .map((r) => r.id);

    const toCreate = args.productIds.filter(
      (r) => !recommendationsIds.includes(r)
    );

    const promises: Promise<any>[] = toCreate.map((id) =>
      this.productRecommendationService.create(ctx, {
        productId: args.productId,
        recommendationId: id,
        type: RecommendationType.CROSSSELL,
      })
    );

    if (toDelete.length > 0) {
      promises.push(this.productRecommendationService.delete(ctx, toDelete));
    }

    await Promise.all(promises);

    return true;
  }
//ProductRecommendationService.ts

@Injectable()
export class ProductRecommendationService {
  constructor(private connection: TransactionalConnection) {}

  findAll(
    ctx: RequestContext,
    options: FindManyOptions<ProductRecommendation> | undefined
  ): Promise<ProductRecommendation[]> {
    return this.connection.getRepository(ctx, ProductRecommendation).find({
      relations: ['product'],
      ...options,
    });
  }

  findOne(
    ctx: RequestContext,
    recommendationId: ID
  ): Promise<ProductRecommendation | null> {
    return this.connection.getRepository(ctx, ProductRecommendation).findOne({
      where: { recommendationId },
      relations: ['product'],
    });
  }

  async create(
    ctx: RequestContext,
    input: ProductRecommendationInput
  ): Promise<ProductRecommendation> {
    const product = await this.connection.getRepository(ctx, Product).findOne({
      where: {
        id: input.productId,
      },
    });

    const recommendation = await this.connection
      .getRepository(ctx, Product)
      .findOne({
        where: {
          id: input.recommendationId,
        },
      });

    if (!product || !recommendation) {
      throw new InternalServerError(
        'error.product-recommendation-could-not-be-fetched'
      );
    }

    const newRecommendation = await this.connection
      .getRepository(ctx, ProductRecommendation)
      .save(
        new ProductRecommendation({
          productId: product.id,
          recommendationId: recommendation.id,
          type: input.type,
        })
      );

    return assertFound(this.findOne(ctx, newRecommendation.id));
  }

  async delete(ctx: RequestContext, ids: ID[]): Promise<DeletionResponse> {
    try {
      await this.connection.getRepository(ctx, ProductRecommendation).delete({
        id: In(ids),
      });

      return {
        result: DeletionResult.DELETED,
      };
    } catch (e) {
      return {
        result: DeletionResult.NOT_DELETED,
        message: e instanceof Error ? e.toString() : '',
      };
    }
  }
}

while we exec mutation updateCrossSellingProducts it will give me

  throw new QueryRunnerAlreadyReleasedError_1.QueryRunnerAlreadyReleasedError();
  exit process 
// product-recommendation.entity.ts

@Entity('product_recommendation')
export class ProductRecommendation extends VendureEntity {
  constructor(input?: DeepPartial<ProductRecommendation>) {
    super(input);
  }

  @Index()
  @Column()
  productId: ID;

  @ManyToOne(() => Product, {
    onDelete: 'CASCADE',
    nullable: false,
  })
  product: Product;

  @Index()
  @Column()
  recommendationId: ID;

  @ManyToOne(() => Product, {
    onDelete: 'CASCADE',
    nullable: false,
  })
  recommendation: Product;

  @Column({
    type: 'varchar',
  })
  type: RecommendationType;
}
 extend type Query {
    productRecommendations(productId: ID!): [ProductRecommendation!]!
  }

  extend type Mutation {
    updateCrossSellingProducts(productId: ID!, productIds: [ID!]!): Boolean!
    updateUpSellingProducts(productId: ID!, productIds: [ID!]!): Boolean!
  }

  extend type Product {
    recommendations: [ProductRecommendation!]!
  }

To Reproduce

Expected behavior

Environment (please complete the following information):

Additional context remove @Transaction() decorator from resolver it works fine.

tufail commented 3 months ago

@tianyingchun did you get the fix, I am having the same issue

tianyingchun commented 3 months ago

Has not been resolved, currently temporarily removing the transaction decorator from the resolver

michaelbromley commented 3 months ago

@tufail what DB are you using?

tianyingchun commented 3 months ago

MySQL