| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- /* eslint-disable @typescript-eslint/restrict-template-expressions */
- import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
- import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
- import {
- Administrator,
- Ctx,
- EventBus,
- InternalServerError,
- NativeAuthenticationMethod,
- PluginCommonModule,
- RequestContext,
- Transaction,
- TransactionalConnection,
- User,
- VendureEvent,
- VendurePlugin,
- } from '@vendure/core';
- import gql from 'graphql-tag';
- import { ReplaySubject, Subscription } from 'rxjs';
- import { vi } from 'vitest';
- export class TestEvent extends VendureEvent {
- constructor(public ctx: RequestContext, public administrator: Administrator) {
- super();
- }
- }
- export const TRIGGER_NO_OPERATION = 'trigger-no-operation';
- export const TRIGGER_ATTEMPTED_UPDATE_EMAIL = 'trigger-attempted-update-email';
- export const TRIGGER_ATTEMPTED_READ_EMAIL = 'trigger-attempted-read-email';
- @Injectable()
- class TestUserService {
- constructor(private connection: TransactionalConnection) {}
- async createUser(ctx: RequestContext, identifier: string) {
- const authMethod = await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(
- new NativeAuthenticationMethod({
- identifier,
- passwordHash: 'abc',
- }),
- );
- await this.connection.getRepository(ctx, User).insert(
- new User({
- authenticationMethods: [authMethod],
- identifier,
- roles: [],
- verified: true,
- }),
- );
- return this.connection.getRepository(ctx, User).findOne({
- where: { identifier },
- });
- }
- }
- @Injectable()
- class TestAdminService {
- constructor(private connection: TransactionalConnection, private userService: TestUserService) {}
- async createAdministrator(ctx: RequestContext, emailAddress: string, fail: boolean) {
- const user = await this.userService.createUser(ctx, emailAddress);
- if (fail) {
- throw new InternalServerError('Failed!');
- }
- const admin = await this.connection.getRepository(ctx, Administrator).save(
- new Administrator({
- emailAddress,
- user,
- firstName: 'jim',
- lastName: 'jiminy',
- }),
- );
- return admin;
- }
- }
- @Resolver()
- class TestResolver {
- constructor(
- private testAdminService: TestAdminService,
- private connection: TransactionalConnection,
- private eventBus: EventBus,
- ) {}
- @Mutation()
- @Transaction()
- async createTestAdministrator(@Ctx() ctx: RequestContext, @Args() args: any) {
- const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
- this.eventBus.publish(new TestEvent(ctx, admin));
- return admin;
- }
- @Mutation()
- @Transaction('manual')
- async createTestAdministrator2(@Ctx() ctx: RequestContext, @Args() args: any) {
- await this.connection.startTransaction(ctx);
- return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
- }
- @Mutation()
- @Transaction('manual')
- async createTestAdministrator3(@Ctx() ctx: RequestContext, @Args() args: any) {
- // no transaction started
- return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
- }
- @Mutation()
- @Transaction()
- async createTestAdministrator4(@Ctx() ctx: RequestContext, @Args() args: any) {
- const admin = await this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
- this.eventBus.publish(new TestEvent(ctx, admin));
- await new Promise(resolve => setTimeout(resolve, 50));
- return admin;
- }
- @Mutation()
- async createTestAdministrator5(@Ctx() ctx: RequestContext, @Args() args: any) {
- if (args.noContext === true) {
- return this.connection.withTransaction(async _ctx => {
- const admin = await this.testAdminService.createAdministrator(
- _ctx,
- args.emailAddress,
- args.fail,
- );
- return admin;
- });
- } else {
- return this.connection.withTransaction(ctx, async _ctx => {
- const admin = await this.testAdminService.createAdministrator(
- _ctx,
- args.emailAddress,
- args.fail,
- );
- return admin;
- });
- }
- }
- @Mutation()
- @Transaction()
- async createNTestAdministrators(@Ctx() ctx: RequestContext, @Args() args: any) {
- let error: any;
- const promises: Array<Promise<any>> = [];
- for (let i = 0; i < args.n; i++) {
- promises.push(
- new Promise(resolve => setTimeout(resolve, i * 10))
- .then(() =>
- this.testAdminService.createAdministrator(
- ctx,
- `${args.emailAddress}${i}`,
- i < args.n * args.failFactor,
- ),
- )
- .then(admin => {
- this.eventBus.publish(new TestEvent(ctx, admin));
- return admin;
- }),
- );
- }
- const result = await Promise.all(promises).catch((e: any) => {
- error = e;
- });
- await this.allSettled(promises);
- if (error) {
- throw error;
- }
- return result;
- }
- @Mutation()
- async createNTestAdministrators2(@Ctx() ctx: RequestContext, @Args() args: any) {
- let error: any;
- const promises: Array<Promise<any>> = [];
- const result = await this.connection
- .withTransaction(ctx, _ctx => {
- for (let i = 0; i < args.n; i++) {
- promises.push(
- new Promise(resolve => setTimeout(resolve, i * 10)).then(() =>
- this.testAdminService.createAdministrator(
- _ctx,
- `${args.emailAddress}${i}`,
- i < args.n * args.failFactor,
- ),
- ),
- );
- }
- return Promise.all(promises);
- })
- .catch((e: any) => {
- error = e;
- });
- await this.allSettled(promises);
- if (error) {
- throw error;
- }
- return result;
- }
- @Mutation()
- @Transaction()
- async createNTestAdministrators3(@Ctx() ctx: RequestContext, @Args() args: any) {
- const result: any[] = [];
- const admin = await this.testAdminService.createAdministrator(
- ctx,
- `${args.emailAddress}${args.n}`,
- args.failFactor >= 1,
- );
- result.push(admin);
- if (args.n > 0) {
- try {
- const admins = await this.connection.withTransaction(ctx, _ctx =>
- this.createNTestAdministrators3(_ctx, {
- ...args,
- n: args.n - 1,
- failFactor: (args.n * args.failFactor) / (args.n - 1),
- }),
- );
- result.push(...admins);
- } catch (e) {
- /* */
- }
- }
- return result;
- }
- @Query()
- async verify() {
- const admins = await this.connection.getRepository(Administrator).find();
- const users = await this.connection.getRepository(User).find();
- return {
- admins,
- users,
- };
- }
- // Promise.allSettled polyfill
- // Same as Promise.all but waits until all promises will be fulfilled or rejected.
- private allSettled<T>(
- promises: Array<Promise<T>>,
- ): Promise<Array<{ status: 'fulfilled'; value: T } | { status: 'rejected'; reason: any }>> {
- return Promise.all(
- promises.map((promise, i) =>
- promise
- .then(value => ({
- status: 'fulfilled' as const,
- value,
- }))
- .catch(reason => ({
- status: 'rejected' as const,
- reason,
- })),
- ),
- );
- }
- }
- @VendurePlugin({
- imports: [PluginCommonModule],
- providers: [TestAdminService, TestUserService],
- adminApiExtensions: {
- schema: gql`
- extend type Mutation {
- createTestAdministrator(emailAddress: String!, fail: Boolean!): Administrator
- createTestAdministrator2(emailAddress: String!, fail: Boolean!): Administrator
- createTestAdministrator3(emailAddress: String!, fail: Boolean!): Administrator
- createTestAdministrator4(emailAddress: String!, fail: Boolean!): Administrator
- createTestAdministrator5(
- emailAddress: String!
- fail: Boolean!
- noContext: Boolean!
- ): Administrator
- createNTestAdministrators(emailAddress: String!, failFactor: Float!, n: Int!): JSON
- createNTestAdministrators2(emailAddress: String!, failFactor: Float!, n: Int!): JSON
- createNTestAdministrators3(emailAddress: String!, failFactor: Float!, n: Int!): JSON
- }
- type VerifyResult {
- admins: [Administrator!]!
- users: [User!]!
- }
- extend type Query {
- verify: VerifyResult!
- }
- `,
- resolvers: [TestResolver],
- },
- })
- export class TransactionTestPlugin implements OnApplicationBootstrap {
- private subscription: Subscription;
- static callHandler = vi.fn();
- static errorHandler = vi.fn();
- static eventHandlerComplete$ = new ReplaySubject(1);
- constructor(private eventBus: EventBus, private connection: TransactionalConnection) {}
- static reset() {
- this.eventHandlerComplete$ = new ReplaySubject(1);
- this.callHandler.mockClear();
- this.errorHandler.mockClear();
- }
- onApplicationBootstrap(): any {
- // This part is used to test how RequestContext with transactions behave
- // when used in an Event subscription
- this.subscription = this.eventBus.ofType(TestEvent).subscribe(async event => {
- const { ctx, administrator } = event;
- if (administrator.emailAddress?.includes(TRIGGER_NO_OPERATION)) {
- TransactionTestPlugin.callHandler();
- TransactionTestPlugin.eventHandlerComplete$.complete();
- }
- if (administrator.emailAddress?.includes(TRIGGER_ATTEMPTED_UPDATE_EMAIL)) {
- TransactionTestPlugin.callHandler();
- const adminRepository = this.connection.getRepository(ctx, Administrator);
- await new Promise(resolve => setTimeout(resolve, 50));
- administrator.lastName = 'modified';
- try {
- await adminRepository.save(administrator);
- } catch (e: any) {
- TransactionTestPlugin.errorHandler(e);
- } finally {
- TransactionTestPlugin.eventHandlerComplete$.complete();
- }
- }
- if (administrator.emailAddress?.includes(TRIGGER_ATTEMPTED_READ_EMAIL)) {
- TransactionTestPlugin.callHandler();
- // note the ctx is not passed here, so we are not inside the ongoing transaction
- const adminRepository = this.connection.getRepository(Administrator);
- try {
- await adminRepository.findOneOrFail({ where: { id: administrator.id } });
- } catch (e: any) {
- TransactionTestPlugin.errorHandler(e);
- } finally {
- TransactionTestPlugin.eventHandlerComplete$.complete();
- }
- }
- });
- }
- }
|