transaction-test-plugin.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. @Mutation()
  101. async createTestAdministrator5(@Ctx() ctx: RequestContext, @Args() args: any) {
  102. if (args.noContext === true) {
  103. return this.connection.withTransaction(ctx, async _ctx => {
  104. const admin = await this.testAdminService.createAdministrator(
  105. _ctx,
  106. args.emailAddress,
  107. args.fail,
  108. );
  109. return admin;
  110. });
  111. } else {
  112. return this.connection.withTransaction(async _ctx => {
  113. const admin = await this.testAdminService.createAdministrator(
  114. _ctx,
  115. args.emailAddress,
  116. args.fail,
  117. );
  118. return admin;
  119. });
  120. }
  121. }
  122. @Query()
  123. async verify() {
  124. const admins = await this.connection.getRepository(Administrator).find();
  125. const users = await this.connection.getRepository(User).find();
  126. return {
  127. admins,
  128. users,
  129. };
  130. }
  131. }
  132. @VendurePlugin({
  133. imports: [PluginCommonModule],
  134. providers: [TestAdminService, TestUserService],
  135. adminApiExtensions: {
  136. schema: gql`
  137. extend type Mutation {
  138. createTestAdministrator(emailAddress: String!, fail: Boolean!): Administrator
  139. createTestAdministrator2(emailAddress: String!, fail: Boolean!): Administrator
  140. createTestAdministrator3(emailAddress: String!, fail: Boolean!): Administrator
  141. createTestAdministrator4(emailAddress: String!, fail: Boolean!): Administrator
  142. createTestAdministrator5(
  143. emailAddress: String!
  144. fail: Boolean!
  145. noContext: Boolean!
  146. ): Administrator
  147. }
  148. type VerifyResult {
  149. admins: [Administrator!]!
  150. users: [User!]!
  151. }
  152. extend type Query {
  153. verify: VerifyResult!
  154. }
  155. `,
  156. resolvers: [TestResolver],
  157. },
  158. })
  159. export class TransactionTestPlugin implements OnApplicationBootstrap {
  160. private subscription: Subscription;
  161. static errorHandler = jest.fn();
  162. static eventHandlerComplete$ = new ReplaySubject(1);
  163. constructor(private eventBus: EventBus, private connection: TransactionalConnection) {}
  164. static reset() {
  165. this.eventHandlerComplete$ = new ReplaySubject(1);
  166. this.errorHandler.mockClear();
  167. }
  168. onApplicationBootstrap(): any {
  169. // This part is used to test how RequestContext with transactions behave
  170. // when used in an Event subscription
  171. this.subscription = this.eventBus.ofType(TestEvent).subscribe(async event => {
  172. const { ctx, administrator } = event;
  173. if (administrator.emailAddress === TRIGGER_ATTEMPTED_UPDATE_EMAIL) {
  174. const adminRepository = this.connection.getRepository(ctx, Administrator);
  175. await new Promise(resolve => setTimeout(resolve, 50));
  176. administrator.lastName = 'modified';
  177. try {
  178. await adminRepository.save(administrator);
  179. } catch (e) {
  180. TransactionTestPlugin.errorHandler(e);
  181. } finally {
  182. TransactionTestPlugin.eventHandlerComplete$.complete();
  183. }
  184. }
  185. if (administrator.emailAddress === TRIGGER_ATTEMPTED_READ_EMAIL) {
  186. // note the ctx is not passed here, so we are not inside the ongoing transaction
  187. const adminRepository = this.connection.getRepository(Administrator);
  188. try {
  189. await adminRepository.findOneOrFail(administrator.id);
  190. } catch (e) {
  191. TransactionTestPlugin.errorHandler(e);
  192. } finally {
  193. TransactionTestPlugin.eventHandlerComplete$.complete();
  194. }
  195. }
  196. });
  197. }
  198. }