transaction-test-plugin.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
  2. import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
  3. import {
  4. Administrator,
  5. Ctx,
  6. EventBus,
  7. InternalServerError,
  8. NativeAuthenticationMethod,
  9. PluginCommonModule,
  10. RequestContext,
  11. Transaction,
  12. TransactionalConnection,
  13. User,
  14. VendureEvent,
  15. VendurePlugin,
  16. } from '@vendure/core';
  17. import gql from 'graphql-tag';
  18. import { ReplaySubject, Subscription } from 'rxjs';
  19. export class TestEvent extends VendureEvent {
  20. constructor(public ctx: RequestContext, public administrator: Administrator) {
  21. super();
  22. }
  23. }
  24. export const TRIGGER_ATTEMPTED_UPDATE_EMAIL = 'trigger-attempted-update-email';
  25. export const TRIGGER_ATTEMPTED_READ_EMAIL = 'trigger-attempted-read-email';
  26. @Injectable()
  27. class TestUserService {
  28. constructor(private connection: TransactionalConnection) {}
  29. async createUser(ctx: RequestContext, identifier: string) {
  30. const authMethod = await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(
  31. new NativeAuthenticationMethod({
  32. identifier,
  33. passwordHash: 'abc',
  34. }),
  35. );
  36. const user = await this.connection.getRepository(ctx, User).save(
  37. new User({
  38. authenticationMethods: [authMethod],
  39. identifier,
  40. roles: [],
  41. verified: true,
  42. }),
  43. );
  44. return user;
  45. }
  46. }
  47. @Injectable()
  48. class TestAdminService {
  49. constructor(private connection: TransactionalConnection, private userService: TestUserService) {}
  50. async createAdministrator(ctx: RequestContext, emailAddress: string, fail: boolean) {
  51. const user = await this.userService.createUser(ctx, emailAddress);
  52. if (fail) {
  53. throw new InternalServerError('Failed!');
  54. }
  55. const admin = await this.connection.getRepository(ctx, Administrator).save(
  56. new Administrator({
  57. emailAddress,
  58. user,
  59. firstName: 'jim',
  60. lastName: 'jiminy',
  61. }),
  62. );
  63. return admin;
  64. }
  65. }
  66. @Resolver()
  67. class TestResolver {
  68. constructor(
  69. private testAdminService: TestAdminService,
  70. private connection: TransactionalConnection,
  71. private eventBus: EventBus,
  72. ) {}
  73. @Mutation()
  74. @Transaction()
  75. async createTestAdministrator(@Ctx() ctx: RequestContext, @Args() args: any) {
  76. const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  77. this.eventBus.publish(new TestEvent(ctx, admin));
  78. return admin;
  79. }
  80. @Mutation()
  81. @Transaction('manual')
  82. async createTestAdministrator2(@Ctx() ctx: RequestContext, @Args() args: any) {
  83. await this.connection.startTransaction(ctx);
  84. return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  85. }
  86. @Mutation()
  87. @Transaction('manual')
  88. async createTestAdministrator3(@Ctx() ctx: RequestContext, @Args() args: any) {
  89. // no transaction started
  90. return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  91. }
  92. @Mutation()
  93. @Transaction()
  94. async createTestAdministrator4(@Ctx() ctx: RequestContext, @Args() args: any) {
  95. const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  96. this.eventBus.publish(new TestEvent(ctx, admin));
  97. await new Promise(resolve => setTimeout(resolve, 50));
  98. return admin;
  99. }
  100. @Query()
  101. async verify() {
  102. const admins = await this.connection.getRepository(Administrator).find();
  103. const users = await this.connection.getRepository(User).find();
  104. return {
  105. admins,
  106. users,
  107. };
  108. }
  109. }
  110. @VendurePlugin({
  111. imports: [PluginCommonModule],
  112. providers: [TestAdminService, TestUserService],
  113. adminApiExtensions: {
  114. schema: gql`
  115. extend type Mutation {
  116. createTestAdministrator(emailAddress: String!, fail: Boolean!): Administrator
  117. createTestAdministrator2(emailAddress: String!, fail: Boolean!): Administrator
  118. createTestAdministrator3(emailAddress: String!, fail: Boolean!): Administrator
  119. createTestAdministrator4(emailAddress: String!, fail: Boolean!): Administrator
  120. }
  121. type VerifyResult {
  122. admins: [Administrator!]!
  123. users: [User!]!
  124. }
  125. extend type Query {
  126. verify: VerifyResult!
  127. }
  128. `,
  129. resolvers: [TestResolver],
  130. },
  131. })
  132. export class TransactionTestPlugin implements OnApplicationBootstrap {
  133. private subscription: Subscription;
  134. static errorHandler = jest.fn();
  135. static eventHandlerComplete$ = new ReplaySubject(1);
  136. constructor(private eventBus: EventBus, private connection: TransactionalConnection) {}
  137. static reset() {
  138. this.eventHandlerComplete$ = new ReplaySubject(1);
  139. this.errorHandler.mockClear();
  140. }
  141. onApplicationBootstrap(): any {
  142. // This part is used to test how RequestContext with transactions behave
  143. // when used in an Event subscription
  144. this.subscription = this.eventBus.ofType(TestEvent).subscribe(async event => {
  145. const { ctx, administrator } = event;
  146. if (administrator.emailAddress === TRIGGER_ATTEMPTED_UPDATE_EMAIL) {
  147. const adminRepository = this.connection.getRepository(ctx, Administrator);
  148. await new Promise(resolve => setTimeout(resolve, 50));
  149. administrator.lastName = 'modified';
  150. try {
  151. await adminRepository.save(administrator);
  152. } catch (e) {
  153. TransactionTestPlugin.errorHandler(e);
  154. } finally {
  155. TransactionTestPlugin.eventHandlerComplete$.complete();
  156. }
  157. }
  158. if (administrator.emailAddress === TRIGGER_ATTEMPTED_READ_EMAIL) {
  159. // note the ctx is not passed here, so we are not inside the ongoing transaction
  160. const adminRepository = this.connection.getRepository(Administrator);
  161. try {
  162. await adminRepository.findOneOrFail(administrator.id);
  163. } catch (e) {
  164. TransactionTestPlugin.errorHandler(e);
  165. } finally {
  166. TransactionTestPlugin.eventHandlerComplete$.complete();
  167. }
  168. }
  169. });
  170. }
  171. }