transaction-test-plugin.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. await this.connection.getRepository(ctx, User).insert(
  37. new User({
  38. authenticationMethods: [authMethod],
  39. identifier,
  40. roles: [],
  41. verified: true,
  42. }),
  43. );
  44. return this.connection.getRepository(ctx, User).findOne({
  45. where: { identifier }
  46. });
  47. }
  48. }
  49. @Injectable()
  50. class TestAdminService {
  51. constructor(private connection: TransactionalConnection, private userService: TestUserService) {}
  52. async createAdministrator(ctx: RequestContext, emailAddress: string, fail: boolean) {
  53. const user = await this.userService.createUser(ctx, emailAddress);
  54. if (fail) {
  55. throw new InternalServerError('Failed!');
  56. }
  57. const admin = await this.connection.getRepository(ctx, Administrator).save(
  58. new Administrator({
  59. emailAddress,
  60. user,
  61. firstName: 'jim',
  62. lastName: 'jiminy',
  63. }),
  64. );
  65. return admin;
  66. }
  67. }
  68. @Resolver()
  69. class TestResolver {
  70. constructor(
  71. private testAdminService: TestAdminService,
  72. private connection: TransactionalConnection,
  73. private eventBus: EventBus,
  74. ) {}
  75. @Mutation()
  76. @Transaction()
  77. async createTestAdministrator(@Ctx() ctx: RequestContext, @Args() args: any) {
  78. const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  79. this.eventBus.publish(new TestEvent(ctx, admin));
  80. return admin;
  81. }
  82. @Mutation()
  83. @Transaction('manual')
  84. async createTestAdministrator2(@Ctx() ctx: RequestContext, @Args() args: any) {
  85. await this.connection.startTransaction(ctx);
  86. return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  87. }
  88. @Mutation()
  89. @Transaction('manual')
  90. async createTestAdministrator3(@Ctx() ctx: RequestContext, @Args() args: any) {
  91. // no transaction started
  92. return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  93. }
  94. @Mutation()
  95. @Transaction()
  96. async createTestAdministrator4(@Ctx() ctx: RequestContext, @Args() args: any) {
  97. const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
  98. this.eventBus.publish(new TestEvent(ctx, admin));
  99. await new Promise(resolve => setTimeout(resolve, 50));
  100. return admin;
  101. }
  102. @Mutation()
  103. async createTestAdministrator5(@Ctx() ctx: RequestContext, @Args() args: any) {
  104. if (args.noContext === true) {
  105. return this.connection.withTransaction(async _ctx => {
  106. const admin = await this.testAdminService.createAdministrator(
  107. _ctx,
  108. args.emailAddress,
  109. args.fail,
  110. );
  111. return admin;
  112. });
  113. } else {
  114. return this.connection.withTransaction(ctx, async _ctx => {
  115. const admin = await this.testAdminService.createAdministrator(
  116. _ctx,
  117. args.emailAddress,
  118. args.fail,
  119. );
  120. return admin;
  121. });
  122. }
  123. }
  124. @Mutation()
  125. @Transaction()
  126. async createNTestAdministrators(@Ctx() ctx: RequestContext, @Args() args: any) {
  127. let error: any;
  128. const promises: Promise<any>[] = []
  129. for (let i = 0; i < args.n; i++) {
  130. promises.push(
  131. new Promise(resolve => setTimeout(resolve, i * 10)).then(() =>
  132. this.testAdminService.createAdministrator(ctx, `${args.emailAddress}${i}`, i < args.n * args.failFactor)
  133. )
  134. )
  135. }
  136. const result = await Promise.all(promises).catch((e: any) => {
  137. error = e;
  138. })
  139. await this.allSettled(promises)
  140. if (error) {
  141. throw error;
  142. }
  143. return result;
  144. }
  145. @Mutation()
  146. async createNTestAdministrators2(@Ctx() ctx: RequestContext, @Args() args: any) {
  147. let error: any;
  148. const promises: Promise<any>[] = []
  149. const result = await this.connection.withTransaction(ctx, _ctx => {
  150. for (let i = 0; i < args.n; i++) {
  151. promises.push(
  152. new Promise(resolve => setTimeout(resolve, i * 10)).then(() =>
  153. this.testAdminService.createAdministrator(_ctx, `${args.emailAddress}${i}`, i < args.n * args.failFactor)
  154. )
  155. )
  156. }
  157. return Promise.all(promises);
  158. }).catch((e: any) => {
  159. error = e;
  160. })
  161. await this.allSettled(promises)
  162. if (error) {
  163. throw error;
  164. }
  165. return result;
  166. }
  167. @Query()
  168. async verify() {
  169. const admins = await this.connection.getRepository(Administrator).find();
  170. const users = await this.connection.getRepository(User).find();
  171. return {
  172. admins,
  173. users,
  174. };
  175. }
  176. // Promise.allSettled polyfill
  177. // Same as Promise.all but waits until all promises will be fulfilled or rejected.
  178. private allSettled<T>(promises: Promise<T>[]): Promise<({status: 'fulfilled', value: T} | { status: 'rejected', reason: any})[]> {
  179. return Promise.all(
  180. promises.map((promise, i) =>
  181. promise
  182. .then(value => ({
  183. status: "fulfilled" as const,
  184. value,
  185. }))
  186. .catch(reason => ({
  187. status: "rejected" as const,
  188. reason,
  189. }))
  190. )
  191. );
  192. }
  193. }
  194. @VendurePlugin({
  195. imports: [PluginCommonModule],
  196. providers: [TestAdminService, TestUserService],
  197. adminApiExtensions: {
  198. schema: gql`
  199. extend type Mutation {
  200. createTestAdministrator(emailAddress: String!, fail: Boolean!): Administrator
  201. createTestAdministrator2(emailAddress: String!, fail: Boolean!): Administrator
  202. createTestAdministrator3(emailAddress: String!, fail: Boolean!): Administrator
  203. createTestAdministrator4(emailAddress: String!, fail: Boolean!): Administrator
  204. createTestAdministrator5(
  205. emailAddress: String!
  206. fail: Boolean!
  207. noContext: Boolean!
  208. ): Administrator
  209. createNTestAdministrators(emailAddress: String!, failFactor: Float!, n: Int!): JSON
  210. createNTestAdministrators2(emailAddress: String!, failFactor: Float!, n: Int!): JSON
  211. }
  212. type VerifyResult {
  213. admins: [Administrator!]!
  214. users: [User!]!
  215. }
  216. extend type Query {
  217. verify: VerifyResult!
  218. }
  219. `,
  220. resolvers: [TestResolver],
  221. },
  222. })
  223. export class TransactionTestPlugin implements OnApplicationBootstrap {
  224. private subscription: Subscription;
  225. static errorHandler = jest.fn();
  226. static eventHandlerComplete$ = new ReplaySubject(1);
  227. constructor(private eventBus: EventBus, private connection: TransactionalConnection) {}
  228. static reset() {
  229. this.eventHandlerComplete$ = new ReplaySubject(1);
  230. this.errorHandler.mockClear();
  231. }
  232. onApplicationBootstrap(): any {
  233. // This part is used to test how RequestContext with transactions behave
  234. // when used in an Event subscription
  235. this.subscription = this.eventBus.ofType(TestEvent).subscribe(async event => {
  236. const { ctx, administrator } = event;
  237. if (administrator.emailAddress === TRIGGER_ATTEMPTED_UPDATE_EMAIL) {
  238. const adminRepository = this.connection.getRepository(ctx, Administrator);
  239. await new Promise(resolve => setTimeout(resolve, 50));
  240. administrator.lastName = 'modified';
  241. try {
  242. await adminRepository.save(administrator);
  243. } catch (e) {
  244. TransactionTestPlugin.errorHandler(e);
  245. } finally {
  246. TransactionTestPlugin.eventHandlerComplete$.complete();
  247. }
  248. }
  249. if (administrator.emailAddress === TRIGGER_ATTEMPTED_READ_EMAIL) {
  250. // note the ctx is not passed here, so we are not inside the ongoing transaction
  251. const adminRepository = this.connection.getRepository(Administrator);
  252. try {
  253. await adminRepository.findOneOrFail(administrator.id);
  254. } catch (e) {
  255. TransactionTestPlugin.errorHandler(e);
  256. } finally {
  257. TransactionTestPlugin.eventHandlerComplete$.complete();
  258. }
  259. }
  260. });
  261. }
  262. }