transaction-test-plugin.ts 12 KB

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