Просмотр исходного кода

refactor(core): Simplify transaction implementation, add manual mode

Michael Bromley 5 лет назад
Родитель
Сommit
5fd5fe681b
32 измененных файлов с 311 добавлено и 192 удалено
  1. 86 7
      packages/core/e2e/database-transactions.e2e-spec.ts
  2. 0 2
      packages/core/src/api/config/configure-graphql-module.ts
  3. 36 3
      packages/core/src/api/decorators/transaction.decorator.ts
  4. 16 4
      packages/core/src/api/middleware/transaction-interceptor.ts
  5. 0 36
      packages/core/src/api/middleware/transaction-plugin.ts
  6. 4 4
      packages/core/src/api/resolvers/admin/administrator.resolver.ts
  7. 4 4
      packages/core/src/api/resolvers/admin/asset.resolver.ts
  8. 3 3
      packages/core/src/api/resolvers/admin/auth.resolver.ts
  9. 3 3
      packages/core/src/api/resolvers/admin/channel.resolver.ts
  10. 4 4
      packages/core/src/api/resolvers/admin/collection.resolver.ts
  11. 7 7
      packages/core/src/api/resolvers/admin/country.resolver.ts
  12. 5 5
      packages/core/src/api/resolvers/admin/customer-group.resolver.ts
  13. 9 9
      packages/core/src/api/resolvers/admin/customer.resolver.ts
  14. 6 6
      packages/core/src/api/resolvers/admin/facet.resolver.ts
  15. 1 1
      packages/core/src/api/resolvers/admin/global-settings.resolver.ts
  16. 10 10
      packages/core/src/api/resolvers/admin/order.resolver.ts
  17. 1 1
      packages/core/src/api/resolvers/admin/payment-method.resolver.ts
  18. 4 4
      packages/core/src/api/resolvers/admin/product-option.resolver.ts
  19. 10 10
      packages/core/src/api/resolvers/admin/product.resolver.ts
  20. 3 3
      packages/core/src/api/resolvers/admin/promotion.resolver.ts
  21. 3 3
      packages/core/src/api/resolvers/admin/role.resolver.ts
  22. 3 3
      packages/core/src/api/resolvers/admin/shipping-method.resolver.ts
  23. 3 3
      packages/core/src/api/resolvers/admin/tax-category.resolver.ts
  24. 3 3
      packages/core/src/api/resolvers/admin/tax-rate.resolver.ts
  25. 5 5
      packages/core/src/api/resolvers/admin/zone.resolver.ts
  26. 11 11
      packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
  27. 4 4
      packages/core/src/api/resolvers/shop/shop-customer.resolver.ts
  28. 13 13
      packages/core/src/api/resolvers/shop/shop-order.resolver.ts
  29. 33 9
      packages/core/src/service/transaction/transactional-connection.ts
  30. 1 3
      packages/dev-server/load-testing/init-load-test.ts
  31. 13 0
      packages/dev-server/load-testing/load-test-config.ts
  32. 7 9
      packages/dev-server/load-testing/scripts/search-and-checkout.js

+ 86 - 7
packages/core/e2e/database-transactions.e2e-spec.ts

@@ -69,11 +69,25 @@ class TestResolver {
     constructor(private testAdminService: TestAdminService, private connection: TransactionalConnection) {}
 
     @Mutation()
-    @Transaction
+    @Transaction()
     createTestAdministrator(@Ctx() ctx: RequestContext, @Args() args: any) {
         return this.testAdminService.createAdministrator(ctx, args.emailAddress, args.fail);
     }
 
+    @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);
+    }
+
     @Query()
     async verify() {
         const admins = await this.connection.getRepository(Administrator).find();
@@ -92,6 +106,8 @@ class TestResolver {
         schema: gql`
             extend type Mutation {
                 createTestAdministrator(emailAddress: String!, fail: Boolean!): Administrator
+                createTestAdministrator2(emailAddress: String!, fail: Boolean!): Administrator
+                createTestAdministrator3(emailAddress: String!, fail: Boolean!): Administrator
             }
             type VerifyResult {
                 admins: [Administrator!]!
@@ -161,19 +177,82 @@ describe('Transaction infrastructure', () => {
         expect(!!verify.admins.find((a: any) => a.emailAddress === 'test2')).toBe(false);
         expect(!!verify.users.find((u: any) => u.identifier === 'test2')).toBe(false);
     });
+
+    it('failing manual mutation', async () => {
+        try {
+            await adminClient.query(CREATE_ADMIN2, {
+                emailAddress: 'test3',
+                fail: true,
+            });
+            fail('Should have thrown');
+        } catch (e) {
+            expect(e.message).toContain('Failed!');
+        }
+
+        const { verify } = await adminClient.query(VERIFY_TEST);
+
+        expect(verify.admins.length).toBe(2);
+        expect(verify.users.length).toBe(2);
+        expect(!!verify.admins.find((a: any) => a.emailAddress === 'test3')).toBe(false);
+        expect(!!verify.users.find((u: any) => u.identifier === 'test3')).toBe(false);
+    });
+
+    it('failing manual mutation without transaction', async () => {
+        try {
+            await adminClient.query(CREATE_ADMIN3, {
+                emailAddress: 'test4',
+                fail: true,
+            });
+            fail('Should have thrown');
+        } catch (e) {
+            expect(e.message).toContain('Failed!');
+        }
+
+        const { verify } = await adminClient.query(VERIFY_TEST);
+
+        expect(verify.admins.length).toBe(2);
+        expect(verify.users.length).toBe(3);
+        expect(!!verify.admins.find((a: any) => a.emailAddress === 'test4')).toBe(false);
+        expect(!!verify.users.find((u: any) => u.identifier === 'test4')).toBe(true);
+    });
 });
 
+const ADMIN_FRAGMENT = gql`
+    fragment CreatedAdmin on Administrator {
+        id
+        emailAddress
+        user {
+            id
+            identifier
+        }
+    }
+`;
+
 const CREATE_ADMIN = gql`
     mutation CreateTestAdmin($emailAddress: String!, $fail: Boolean!) {
         createTestAdministrator(emailAddress: $emailAddress, fail: $fail) {
-            id
-            emailAddress
-            user {
-                id
-                identifier
-            }
+            ...CreatedAdmin
+        }
+    }
+    ${ADMIN_FRAGMENT}
+`;
+
+const CREATE_ADMIN2 = gql`
+    mutation CreateTestAdmin2($emailAddress: String!, $fail: Boolean!) {
+        createTestAdministrator2(emailAddress: $emailAddress, fail: $fail) {
+            ...CreatedAdmin
+        }
+    }
+    ${ADMIN_FRAGMENT}
+`;
+
+const CREATE_ADMIN3 = gql`
+    mutation CreateTestAdmin2($emailAddress: String!, $fail: Boolean!) {
+        createTestAdministrator3(emailAddress: $emailAddress, fail: $fail) {
+            ...CreatedAdmin
         }
     }
+    ${ADMIN_FRAGMENT}
 `;
 
 const VERIFY_TEST = gql`

+ 0 - 2
packages/core/src/api/config/configure-graphql-module.ts

@@ -18,7 +18,6 @@ import { ApiSharedModule } from '../api-internal-modules';
 import { IdCodecService } from '../common/id-codec.service';
 import { AssetInterceptorPlugin } from '../middleware/asset-interceptor-plugin';
 import { IdCodecPlugin } from '../middleware/id-codec-plugin';
-import { TransactionPlugin } from '../middleware/transaction-plugin';
 import { TranslateErrorsPlugin } from '../middleware/translate-errors-plugin';
 
 import { generateAuthenticationTypes } from './generate-auth-types';
@@ -147,7 +146,6 @@ async function createGraphQLOptions(
             new IdCodecPlugin(idCodecService),
             new TranslateErrorsPlugin(i18nService),
             new AssetInterceptorPlugin(configService),
-            new TransactionPlugin(),
             ...configService.apiOptions.apolloServerPlugins,
         ],
     } as GqlModuleOptions;

+ 36 - 3
packages/core/src/api/decorators/transaction.decorator.ts

@@ -1,7 +1,20 @@
-import { applyDecorators, UseInterceptors } from '@nestjs/common';
+import { applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common';
 
 import { TransactionInterceptor } from '../middleware/transaction-interceptor';
 
+export const TRANSACTION_MODE_METADATA_KEY = '__transaction_mode__';
+/**
+ * @description
+ * When in `auto` mode, a transaction will be automatically started before the resolver
+ * runs and committed after it ends (or rolled back in case of error).
+ *
+ * In `manual` mode, the application code itself must start and commit the transactions.
+ *
+ * @default 'auto'
+ * @docsCategory data-access
+ */
+export type TransactionMode = 'auto' | 'manual';
+
 /**
  * @description
  * Runs the decorated method in a TypeORM transaction. It works by creating a transctional
@@ -9,7 +22,27 @@ import { TransactionInterceptor } from '../middleware/transaction-interceptor';
  * is the passed to the {@link TransactionalConnection} `getRepository()` method, this
  * QueryRunner is used to execute the queries within this transaction.
  *
+ * @example
+ * ```TypeScript
+ * // in a GraphQL resolver file
+ *
+ * \@Transaction()
+ * async myMutation(@Ctx() ctx: RequestContext) {
+ *   // as long as the `ctx` object is passed in to
+ *   // all database operations, the entire mutation
+ *   // will be run as an atomic transaction, and rolled
+ *   // back if an error is thrown.
+ *   const result = this.myService.createThing(ctx);
+ *   return this.myService.updateOtherThing(ctx, result.id);
+ * }
+ * ```
+ *
  * @docsCategory request
- * @docsPage Decorators
+ * @docsCategory data-access
  */
-export const Transaction = applyDecorators(UseInterceptors(TransactionInterceptor));
+export const Transaction = (transactionMode: TransactionMode = 'auto') => {
+    return applyDecorators(
+        SetMetadata(TRANSACTION_MODE_METADATA_KEY, transactionMode),
+        UseInterceptors(TransactionInterceptor),
+    );
+};

+ 16 - 4
packages/core/src/api/middleware/transaction-interceptor.ts

@@ -1,10 +1,12 @@
 import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
 import { Observable, of } from 'rxjs';
 
 import { REQUEST_CONTEXT_KEY, TRANSACTION_MANAGER_KEY } from '../../common/constants';
 import { TransactionalConnection } from '../../service/transaction/transactional-connection';
 import { parseContext } from '../common/parse-context';
 import { RequestContext } from '../common/request-context';
+import { TRANSACTION_MODE_METADATA_KEY, TransactionMode } from '../decorators/transaction.decorator';
 
 /**
  * @description
@@ -13,12 +15,16 @@ import { RequestContext } from '../common/request-context';
  */
 @Injectable()
 export class TransactionInterceptor implements NestInterceptor {
-    constructor(private connection: TransactionalConnection) {}
+    constructor(private connection: TransactionalConnection, private reflector: Reflector) {}
     intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
         const { isGraphQL, req } = parseContext(context);
         const ctx = (req as any)[REQUEST_CONTEXT_KEY];
         if (ctx) {
-            return of(this.withTransaction(ctx, () => next.handle().toPromise(), context.getHandler().name));
+            const transactionMode = this.reflector.get<TransactionMode>(
+                TRANSACTION_MODE_METADATA_KEY,
+                context.getHandler(),
+            );
+            return of(this.withTransaction(ctx, () => next.handle().toPromise(), transactionMode));
         } else {
             return next.handle();
         }
@@ -28,7 +34,7 @@ export class TransactionInterceptor implements NestInterceptor {
      * @description
      * Executes the `work` function within the context of a transaction.
      */
-    private async withTransaction<T>(ctx: RequestContext, work: () => T, handler: any): Promise<T> {
+    private async withTransaction<T>(ctx: RequestContext, work: () => T, mode: TransactionMode): Promise<T> {
         const queryRunnerExists = !!(ctx as any)[TRANSACTION_MANAGER_KEY];
         if (queryRunnerExists) {
             // If a QueryRunner already exists on the RequestContext, there must be an existing
@@ -37,7 +43,9 @@ export class TransactionInterceptor implements NestInterceptor {
             return work();
         }
         const queryRunner = this.connection.rawConnection.createQueryRunner();
-        await queryRunner.startTransaction();
+        if (mode === 'auto') {
+            await queryRunner.startTransaction();
+        }
         (ctx as any)[TRANSACTION_MANAGER_KEY] = queryRunner.manager;
 
         try {
@@ -51,6 +59,10 @@ export class TransactionInterceptor implements NestInterceptor {
                 await queryRunner.rollbackTransaction();
             }
             throw error;
+        } finally {
+            if (queryRunner?.isReleased === false) {
+                await queryRunner.release();
+            }
         }
     }
 }

+ 0 - 36
packages/core/src/api/middleware/transaction-plugin.ts

@@ -1,36 +0,0 @@
-import { ApolloServerPlugin, GraphQLRequestListener, GraphQLServiceContext } from 'apollo-server-plugin-base';
-import { EntityManager } from 'typeorm';
-
-import { REQUEST_CONTEXT_KEY, TRANSACTION_MANAGER_KEY } from '../../common/constants';
-import { AssetStorageStrategy } from '../../config/asset-storage-strategy/asset-storage-strategy';
-import { TransactionalConnection } from '../../service/transaction/transactional-connection';
-import { RequestContext } from '../common/request-context';
-
-/**
- * @description
- * Intercepts outgoing responses to see if there is an open QueryRunner attached to the
- * RequestContext. This is necessary when using the {@link TransactionInterceptor} because
- * it opens a transaction without releasing it.
- *
- * The reason that the `.release()` call is done here, and not in a `finally` block
- * in the TransactionInterceptor is that this plugin runs after _all nested resolvers_
- * have resolved, whereas the Interceptor considers the request complete after only the
- * top-level resolver returns.
- */
-export class TransactionPlugin implements ApolloServerPlugin {
-    requestDidStart(): GraphQLRequestListener {
-        return {
-            willSendResponse: async requestContext => {
-                const { context } = requestContext;
-                const transactionManager: EntityManager | undefined =
-                    context.req?.[REQUEST_CONTEXT_KEY]?.[TRANSACTION_MANAGER_KEY];
-                if (transactionManager) {
-                    const { queryRunner } = transactionManager;
-                    if (queryRunner?.isReleased === false) {
-                        await queryRunner.release();
-                    }
-                }
-            },
-        };
-    }
-}

+ 4 - 4
packages/core/src/api/resolvers/admin/administrator.resolver.ts

@@ -40,7 +40,7 @@ export class AdministratorResolver {
         return this.administratorService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateAdministrator)
     createAdministrator(
@@ -51,7 +51,7 @@ export class AdministratorResolver {
         return this.administratorService.create(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateAdministrator)
     updateAdministrator(
@@ -62,7 +62,7 @@ export class AdministratorResolver {
         return this.administratorService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateAdministrator)
     assignRoleToAdministrator(
@@ -72,7 +72,7 @@ export class AdministratorResolver {
         return this.administratorService.assignRole(ctx, args.administratorId, args.roleId);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteAdministrator)
     deleteAdministrator(

+ 4 - 4
packages/core/src/api/resolvers/admin/asset.resolver.ts

@@ -33,7 +33,7 @@ export class AssetResolver {
         return this.assetService.findAll(ctx, args.options || undefined);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createAssets(@Ctx() ctx: RequestContext, @Args() args: MutationCreateAssetsArgs): Promise<Asset[]> {
@@ -52,21 +52,21 @@ export class AssetResolver {
         return assets;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateAsset(@Ctx() ctx: RequestContext, @Args() { input }: MutationUpdateAssetArgs) {
         return this.assetService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteAsset(@Ctx() ctx: RequestContext, @Args() { id, force }: MutationDeleteAssetArgs) {
         return this.assetService.delete(ctx, [id], force || undefined);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteAssets(@Ctx() ctx: RequestContext, @Args() { ids, force }: MutationDeleteAssetsArgs) {

+ 3 - 3
packages/core/src/api/resolvers/admin/auth.resolver.ts

@@ -30,7 +30,7 @@ export class AuthResolver extends BaseAuthResolver {
         super(authService, userService, administratorService, configService);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     login(
@@ -42,7 +42,7 @@ export class AuthResolver extends BaseAuthResolver {
         return super.login(args, ctx, req, res);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     authenticate(
@@ -54,7 +54,7 @@ export class AuthResolver extends BaseAuthResolver {
         return this.authenticateAndCreateSession(ctx, args, req, res);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     logout(

+ 3 - 3
packages/core/src/api/resolvers/admin/channel.resolver.ts

@@ -38,7 +38,7 @@ export class ChannelResolver {
         return ctx.channel;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.SuperAdmin)
     async createChannel(
@@ -53,7 +53,7 @@ export class ChannelResolver {
         return channel;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.SuperAdmin)
     async updateChannel(
@@ -63,7 +63,7 @@ export class ChannelResolver {
         return this.channelService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.SuperAdmin)
     async deleteChannel(

+ 4 - 4
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -75,7 +75,7 @@ export class CollectionResolver {
         return this.encodeFilters(collection);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createCollection(
@@ -87,7 +87,7 @@ export class CollectionResolver {
         return this.collectionService.create(ctx, input).then(this.encodeFilters);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateCollection(
@@ -99,7 +99,7 @@ export class CollectionResolver {
         return this.collectionService.update(ctx, input).then(this.encodeFilters);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async moveCollection(
@@ -110,7 +110,7 @@ export class CollectionResolver {
         return this.collectionService.move(ctx, input).then(this.encodeFilters);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteCollection(

+ 7 - 7
packages/core/src/api/resolvers/admin/country.resolver.ts

@@ -1,12 +1,12 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
-    QueryCountriesArgs,
-    QueryCountryArgs,
+    DeletionResponse,
     MutationCreateCountryArgs,
     MutationDeleteCountryArgs,
-    DeletionResponse,
-    Permission,
     MutationUpdateCountryArgs,
+    Permission,
+    QueryCountriesArgs,
+    QueryCountryArgs,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
@@ -40,7 +40,7 @@ export class CountryResolver {
         return this.countryService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateSettings)
     async createCountry(
@@ -50,7 +50,7 @@ export class CountryResolver {
         return this.countryService.create(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async updateCountry(
@@ -60,7 +60,7 @@ export class CountryResolver {
         return this.countryService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteSettings)
     async deleteCountry(

+ 5 - 5
packages/core/src/api/resolvers/admin/customer-group.resolver.ts

@@ -41,7 +41,7 @@ export class CustomerGroupResolver {
         return this.customerGroupService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCustomer)
     async createCustomerGroup(
@@ -51,7 +51,7 @@ export class CustomerGroupResolver {
         return this.customerGroupService.create(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async updateCustomerGroup(
@@ -61,7 +61,7 @@ export class CustomerGroupResolver {
         return this.customerGroupService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCustomer)
     async deleteCustomerGroup(
@@ -71,7 +71,7 @@ export class CustomerGroupResolver {
         return this.customerGroupService.delete(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async addCustomersToGroup(
@@ -81,7 +81,7 @@ export class CustomerGroupResolver {
         return this.customerGroupService.addCustomersToGroup(ctx, args);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async removeCustomersFromGroup(

+ 9 - 9
packages/core/src/api/resolvers/admin/customer.resolver.ts

@@ -48,7 +48,7 @@ export class CustomerResolver {
         return this.customerService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCustomer)
     async createCustomer(
@@ -59,7 +59,7 @@ export class CustomerResolver {
         return this.customerService.create(ctx, input, password || undefined);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async updateCustomer(
@@ -70,7 +70,7 @@ export class CustomerResolver {
         return this.customerService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCustomer)
     async createCustomerAddress(
@@ -81,7 +81,7 @@ export class CustomerResolver {
         return this.customerService.createAddress(ctx, customerId, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async updateCustomerAddress(
@@ -92,7 +92,7 @@ export class CustomerResolver {
         return this.customerService.updateAddress(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCustomer)
     async deleteCustomerAddress(
@@ -103,7 +103,7 @@ export class CustomerResolver {
         return this.customerService.deleteAddress(ctx, id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCustomer)
     async deleteCustomer(
@@ -113,21 +113,21 @@ export class CustomerResolver {
         return this.customerService.softDelete(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async addNoteToCustomer(@Ctx() ctx: RequestContext, @Args() args: MutationAddNoteToCustomerArgs) {
         return this.customerService.addNoteToCustomer(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async updateCustomerNote(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateCustomerNoteArgs) {
         return this.customerService.updateCustomerNote(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCustomer)
     async deleteCustomerNote(@Ctx() ctx: RequestContext, @Args() args: MutationDeleteCustomerNoteArgs) {

+ 6 - 6
packages/core/src/api/resolvers/admin/facet.resolver.ts

@@ -51,7 +51,7 @@ export class FacetResolver {
         return this.facetService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createFacet(
@@ -70,7 +70,7 @@ export class FacetResolver {
         return facet;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateFacet(
@@ -81,7 +81,7 @@ export class FacetResolver {
         return this.facetService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteFacet(
@@ -91,7 +91,7 @@ export class FacetResolver {
         return this.facetService.delete(ctx, args.id, args.force || false);
     }
 
-    // @Transaction
+    // @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createFacetValues(
@@ -112,7 +112,7 @@ export class FacetResolver {
         );
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateFacetValues(
@@ -123,7 +123,7 @@ export class FacetResolver {
         return Promise.all(input.map(facetValue => this.facetValueService.update(ctx, facetValue)));
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteFacetValues(

+ 1 - 1
packages/core/src/api/resolvers/admin/global-settings.resolver.ts

@@ -54,7 +54,7 @@ export class GlobalSettingsResolver {
         };
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async updateGlobalSettings(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateGlobalSettingsArgs) {

+ 10 - 10
packages/core/src/api/resolvers/admin/order.resolver.ts

@@ -41,70 +41,70 @@ export class OrderResolver {
         return this.orderService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async settlePayment(@Ctx() ctx: RequestContext, @Args() args: MutationSettlePaymentArgs) {
         return this.orderService.settlePayment(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async fulfillOrder(@Ctx() ctx: RequestContext, @Args() args: MutationFulfillOrderArgs) {
         return this.orderService.createFulfillment(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async cancelOrder(@Ctx() ctx: RequestContext, @Args() args: MutationCancelOrderArgs) {
         return this.orderService.cancelOrder(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async refundOrder(@Ctx() ctx: RequestContext, @Args() args: MutationRefundOrderArgs) {
         return this.orderService.refundOrder(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async settleRefund(@Ctx() ctx: RequestContext, @Args() args: MutationSettleRefundArgs) {
         return this.orderService.settleRefund(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async addNoteToOrder(@Ctx() ctx: RequestContext, @Args() args: MutationAddNoteToOrderArgs) {
         return this.orderService.addNoteToOrder(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async updateOrderNote(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateOrderNoteArgs) {
         return this.orderService.updateOrderNote(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async deleteOrderNote(@Ctx() ctx: RequestContext, @Args() args: MutationDeleteOrderNoteArgs) {
         return this.orderService.deleteOrderNote(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async setOrderCustomFields(@Ctx() ctx: RequestContext, @Args() args: MutationSetOrderCustomFieldsArgs) {
         return this.orderService.updateCustomFields(ctx, args.input.id, args.input.customFields);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     async transitionOrderToState(

+ 1 - 1
packages/core/src/api/resolvers/admin/payment-method.resolver.ts

@@ -36,7 +36,7 @@ export class PaymentMethodResolver {
         return this.paymentMethodService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     updatePaymentMethod(

+ 4 - 4
packages/core/src/api/resolvers/admin/product-option.resolver.ts

@@ -44,7 +44,7 @@ export class ProductOptionResolver {
         return this.productOptionGroupService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createProductOptionGroup(
@@ -63,7 +63,7 @@ export class ProductOptionResolver {
         return group;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateProductOptionGroup(
@@ -74,7 +74,7 @@ export class ProductOptionResolver {
         return this.productOptionGroupService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createProductOption(
@@ -85,7 +85,7 @@ export class ProductOptionResolver {
         return this.productOptionService.create(ctx, input.productOptionGroupId, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateProductOption(

+ 10 - 10
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -75,7 +75,7 @@ export class ProductResolver {
         return this.productVariantService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     async createProduct(
@@ -86,7 +86,7 @@ export class ProductResolver {
         return this.productService.create(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateProduct(
@@ -97,7 +97,7 @@ export class ProductResolver {
         return await this.productService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteProduct(
@@ -107,7 +107,7 @@ export class ProductResolver {
         return this.productService.softDelete(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async addOptionGroupToProduct(
@@ -118,7 +118,7 @@ export class ProductResolver {
         return this.productService.addOptionGroupToProduct(ctx, productId, optionGroupId);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async removeOptionGroupFromProduct(
@@ -129,7 +129,7 @@ export class ProductResolver {
         return this.productService.removeOptionGroupFromProduct(ctx, productId, optionGroupId);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async createProductVariants(
@@ -140,7 +140,7 @@ export class ProductResolver {
         return this.productVariantService.create(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async updateProductVariants(
@@ -151,7 +151,7 @@ export class ProductResolver {
         return this.productVariantService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteCatalog)
     async deleteProductVariant(
@@ -161,7 +161,7 @@ export class ProductResolver {
         return this.productVariantService.softDelete(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async assignProductsToChannel(
@@ -171,7 +171,7 @@ export class ProductResolver {
         return this.productService.assignProductsToChannel(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     async removeProductsFromChannel(

+ 3 - 3
packages/core/src/api/resolvers/admin/promotion.resolver.ts

@@ -57,7 +57,7 @@ export class PromotionResolver {
         return this.promotionService.getPromotionActions(ctx);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreatePromotion)
     createPromotion(
@@ -75,7 +75,7 @@ export class PromotionResolver {
         return this.promotionService.createPromotion(ctx, args.input).then(this.encodeConditionsAndActions);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdatePromotion)
     updatePromotion(
@@ -97,7 +97,7 @@ export class PromotionResolver {
         return this.promotionService.updatePromotion(ctx, args.input).then(this.encodeConditionsAndActions);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeletePromotion)
     deletePromotion(

+ 3 - 3
packages/core/src/api/resolvers/admin/role.resolver.ts

@@ -33,7 +33,7 @@ export class RoleResolver {
         return this.roleService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateAdministrator)
     createRole(@Ctx() ctx: RequestContext, @Args() args: MutationCreateRoleArgs): Promise<Role> {
@@ -41,7 +41,7 @@ export class RoleResolver {
         return this.roleService.create(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateAdministrator)
     updateRole(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateRoleArgs): Promise<Role> {
@@ -49,7 +49,7 @@ export class RoleResolver {
         return this.roleService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteAdministrator)
     deleteRole(@Ctx() ctx: RequestContext, @Args() args: MutationDeleteRoleArgs): Promise<DeletionResponse> {

+ 3 - 3
packages/core/src/api/resolvers/admin/shipping-method.resolver.ts

@@ -58,7 +58,7 @@ export class ShippingMethodResolver {
         return this.shippingMethodService.getShippingCalculators(ctx);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateSettings)
     createShippingMethod(
@@ -69,7 +69,7 @@ export class ShippingMethodResolver {
         return this.shippingMethodService.create(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     updateShippingMethod(
@@ -80,7 +80,7 @@ export class ShippingMethodResolver {
         return this.shippingMethodService.update(ctx, input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteSettings)
     deleteShippingMethod(

+ 3 - 3
packages/core/src/api/resolvers/admin/tax-category.resolver.ts

@@ -34,7 +34,7 @@ export class TaxCategoryResolver {
         return this.taxCategoryService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateSettings)
     async createTaxCategory(
@@ -44,7 +44,7 @@ export class TaxCategoryResolver {
         return this.taxCategoryService.create(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async updateTaxCategory(
@@ -54,7 +54,7 @@ export class TaxCategoryResolver {
         return this.taxCategoryService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteSettings)
     async deleteTaxCategory(

+ 3 - 3
packages/core/src/api/resolvers/admin/tax-rate.resolver.ts

@@ -33,7 +33,7 @@ export class TaxRateResolver {
         return this.taxRateService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateSettings)
     async createTaxRate(
@@ -43,7 +43,7 @@ export class TaxRateResolver {
         return this.taxRateService.create(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async updateTaxRate(
@@ -53,7 +53,7 @@ export class TaxRateResolver {
         return this.taxRateService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteSettings)
     async deleteTaxRate(

+ 5 - 5
packages/core/src/api/resolvers/admin/zone.resolver.ts

@@ -33,21 +33,21 @@ export class ZoneResolver {
         return this.zoneService.findOne(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.CreateSettings)
     async createZone(@Ctx() ctx: RequestContext, @Args() args: MutationCreateZoneArgs): Promise<Zone> {
         return this.zoneService.create(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async updateZone(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateZoneArgs): Promise<Zone> {
         return this.zoneService.update(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.DeleteSettings)
     async deleteZone(
@@ -57,7 +57,7 @@ export class ZoneResolver {
         return this.zoneService.delete(ctx, args.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async addMembersToZone(
@@ -67,7 +67,7 @@ export class ZoneResolver {
         return this.zoneService.addMembersToZone(ctx, args);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     async removeMembersFromZone(

+ 11 - 11
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -55,7 +55,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         );
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     login(
@@ -68,7 +68,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return super.login(args, ctx, req, res);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     authenticate(
@@ -80,7 +80,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return this.authenticateAndCreateSession(ctx, args, req, res);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     logout(
@@ -97,7 +97,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return super.me(ctx, 'shop');
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     async registerCustomerAccount(
@@ -108,7 +108,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return this.customerService.registerCustomerAccount(ctx, args.input).then(() => true);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     async verifyCustomerAccount(
@@ -145,7 +145,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     async refreshCustomerVerification(
@@ -156,14 +156,14 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return this.customerService.refreshVerificationToken(ctx, args.emailAddress).then(() => true);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     async requestPasswordReset(@Ctx() ctx: RequestContext, @Args() args: MutationRequestPasswordResetArgs) {
         return this.customerService.requestPasswordReset(ctx, args.emailAddress).then(() => true);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Public)
     async resetPassword(
@@ -194,7 +194,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async updateCustomerPassword(
@@ -217,7 +217,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return result;
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async requestUpdateCustomerEmailAddress(
@@ -232,7 +232,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return this.customerService.requestUpdateEmailAddress(ctx, ctx.activeUserId, args.newEmailAddress);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async updateCustomerEmailAddress(

+ 4 - 4
packages/core/src/api/resolvers/shop/shop-customer.resolver.ts

@@ -29,7 +29,7 @@ export class ShopCustomerResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async updateCustomer(
@@ -43,7 +43,7 @@ export class ShopCustomerResolver {
         });
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async createCustomerAddress(
@@ -54,7 +54,7 @@ export class ShopCustomerResolver {
         return this.customerService.createAddress(ctx, customer.id, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async updateCustomerAddress(
@@ -69,7 +69,7 @@ export class ShopCustomerResolver {
         return this.customerService.updateAddress(ctx, args.input);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async deleteCustomerAddress(

+ 13 - 13
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -123,7 +123,7 @@ export class ShopOrderResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async setOrderShippingAddress(
@@ -140,7 +140,7 @@ export class ShopOrderResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async setOrderBillingAddress(
@@ -169,7 +169,7 @@ export class ShopOrderResolver {
         return [];
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async setOrderShippingMethod(
@@ -184,7 +184,7 @@ export class ShopOrderResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async setOrderCustomFields(
@@ -209,7 +209,7 @@ export class ShopOrderResolver {
         return [];
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async transitionOrderToState(
@@ -222,7 +222,7 @@ export class ShopOrderResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async addItemToOrder(
@@ -239,7 +239,7 @@ export class ShopOrderResolver {
         );
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async adjustOrderLine(
@@ -259,7 +259,7 @@ export class ShopOrderResolver {
         );
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeOrderLine(
@@ -270,7 +270,7 @@ export class ShopOrderResolver {
         return this.orderService.removeItemFromOrder(ctx, order.id, args.orderLineId);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeAllOrderLines(@Ctx() ctx: RequestContext): Promise<Order> {
@@ -278,7 +278,7 @@ export class ShopOrderResolver {
         return this.orderService.removeAllItemsFromOrder(ctx, order.id);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async applyCouponCode(
@@ -289,7 +289,7 @@ export class ShopOrderResolver {
         return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async removeCouponCode(
@@ -300,7 +300,7 @@ export class ShopOrderResolver {
         return this.orderService.removeCouponCode(ctx, order.id, args.couponCode);
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
     async addPaymentToOrder(@Ctx() ctx: RequestContext, @Args() args: MutationAddPaymentToOrderArgs) {
@@ -337,7 +337,7 @@ export class ShopOrderResolver {
         }
     }
 
-    @Transaction
+    @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
     async setCustomerForOrder(@Ctx() ctx: RequestContext, @Args() args: MutationSetCustomerForOrderArgs) {

+ 33 - 9
packages/core/src/service/transaction/transactional-connection.ts

@@ -5,14 +5,12 @@ import {
     Connection,
     EntityManager,
     EntitySchema,
-    FindManyOptions,
     FindOneOptions,
     FindOptionsUtils,
     getRepository,
     ObjectType,
     Repository,
 } from 'typeorm';
-import { RepositoryFactory } from 'typeorm/repository/RepositoryFactory';
 
 import { RequestContext } from '../../api/common/request-context';
 import { TRANSACTION_MANAGER_KEY } from '../../common/constants';
@@ -20,6 +18,9 @@ import { EntityNotFoundError } from '../../common/error/errors';
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
 import { VendureEntity } from '../../entity/base/base.entity';
 
+/**
+ * @docsCategory data-access
+ */
 export interface FindEntityOptions extends FindOneOptions {
     channelId?: ID;
 }
@@ -72,10 +73,9 @@ export class TransactionalConnection {
         maybeTarget?: ObjectType<Entity> | EntitySchema<Entity> | string,
     ): Repository<Entity> {
         if (ctxOrTarget instanceof RequestContext) {
-            const transactionManager = (ctxOrTarget as any)[TRANSACTION_MANAGER_KEY];
-            if (transactionManager && maybeTarget) {
-                const metadata = this.connection.getMetadata(maybeTarget);
-                return new RepositoryFactory().create(transactionManager, metadata);
+            const transactionManager = this.getTransactionManager(ctxOrTarget);
+            if (transactionManager && maybeTarget && !transactionManager.queryRunner?.isReleased) {
+                return transactionManager.getRepository(maybeTarget);
             } else {
                 // tslint:disable-next-line:no-non-null-assertion
                 return getRepository(maybeTarget!);
@@ -86,19 +86,37 @@ export class TransactionalConnection {
         }
     }
 
+    /**
+     * @description
+     * Manually start a transaction if one is not already in progress. This method should be used in
+     * conjunction with the `'manual'` mode of the {@link Transaction} decorator.
+     */
+    async startTransaction(ctx: RequestContext) {
+        const transactionManager = this.getTransactionManager(ctx);
+        if (transactionManager?.queryRunner?.isTransactionActive === false) {
+            await transactionManager.queryRunner.startTransaction();
+        }
+    }
+
     /**
      * @description
      * Manually commits any open transaction. Should be very rarely needed, since the {@link Transaction} decorator
      * and the {@link TransactionInterceptor} take care of this automatically. Use-cases include situations
-     * in which the worker thread needs to access changes made in the current transaction.
+     * in which the worker thread needs to access changes made in the current transaction, or when using the
+     * Transaction decorator in manual mode.
      */
     async commitOpenTransaction(ctx: RequestContext) {
-        const transactionManager: EntityManager = (ctx as any)[TRANSACTION_MANAGER_KEY];
-        if (transactionManager.queryRunner?.isTransactionActive) {
+        const transactionManager = this.getTransactionManager(ctx);
+        if (transactionManager?.queryRunner?.isTransactionActive) {
             await transactionManager.queryRunner.commitTransaction();
         }
     }
 
+    /**
+     * @description
+     * Finds an entity of the given type by ID, or throws an `EntityNotFoundError` if none
+     * is found.
+     */
     async getEntityOrThrow<T extends VendureEntity>(
         ctx: RequestContext,
         entityType: Type<T>,
@@ -121,6 +139,7 @@ export class TransactionalConnection {
     }
 
     /**
+     * @description
      * Like the TypeOrm `Repository.findOne()` method, but limits the results to
      * the given Channel.
      */
@@ -145,6 +164,7 @@ export class TransactionalConnection {
     }
 
     /**
+     * @description
      * Like the TypeOrm `Repository.findByIds()` method, but limits the results to
      * the given Channel.
      */
@@ -173,4 +193,8 @@ export class TransactionalConnection {
             .andWhere('channel.id = :channelId', { channelId })
             .getMany();
     }
+
+    private getTransactionManager(ctx: RequestContext): EntityManager | undefined {
+        return (ctx as any)[TRANSACTION_MANAGER_KEY];
+    }
 }

+ 1 - 3
packages/dev-server/load-testing/init-load-test.ts

@@ -247,9 +247,7 @@ const parts = [
 ];
 function generateProductDescription(): string {
     const take = Math.ceil(Math.random() * 4);
-    return shuffle(parts)
-        .slice(0, take)
-        .join('. ');
+    return shuffle(parts).slice(0, take).join('. ');
 }
 
 /**

+ 13 - 0
packages/dev-server/load-testing/load-test-config.ts

@@ -21,6 +21,19 @@ export function getMysqlConnectionOptions(count: number) {
         username: 'root',
         password: '',
         database: `vendure-load-testing-${count}`,
+        extra: {
+            // connectionLimit: 150,
+        },
+    };
+}
+export function getPostgresConnectionOptions(count: number) {
+    return {
+        type: 'postgres' as const,
+        host: '127.0.0.1',
+        port: 5432,
+        username: 'admin',
+        password: 'secret',
+        database: `vendure-load-testing-${count}`,
     };
 }
 

+ 7 - 9
packages/dev-server/load-testing/scripts/search-and-checkout.js

@@ -1,7 +1,7 @@
 // @ts-check
-import {sleep} from 'k6';
+import { sleep } from 'k6';
 import { check } from 'k6';
-import {ShopApiRequest} from '../utils/api-request.js';
+import { ShopApiRequest } from '../utils/api-request.js';
 
 const searchQuery = new ShopApiRequest('shop/search.graphql');
 const productQuery = new ShopApiRequest('shop/product.graphql');
@@ -11,18 +11,16 @@ const getShippingMethodsQuery = new ShopApiRequest('shop/get-shipping-methods.gr
 const completeOrderMutation = new ShopApiRequest('shop/complete-order.graphql');
 
 export let options = {
-    stages: [
-        { duration: '4m', target: 500 },
-    ],
+    stages: [{ duration: '4m', target: 500 }],
 };
 
 /**
  * Searches for products, adds to order, checks out.
  */
-export default function() {
+export default function () {
     const itemsToAdd = Math.ceil(Math.random() * 10);
 
-    for (let i = 0; i < itemsToAdd; i ++) {
+    for (let i = 0; i < itemsToAdd; i++) {
         searchProducts();
         const product = findAndLoadProduct();
         addToCart(randomItem(product.variants).id);
@@ -53,8 +51,8 @@ function addToCart(variantId) {
     const qty = Math.ceil(Math.random() * 4);
     const result = addItemToOrderMutation.post({ id: variantId, qty });
     check(result.data, {
-        'Product added to cart': r => !!r.addItemToOrder.lines
-            .find(l => l.productVariant.id === variantId && l.quantity >= qty),
+        'Product added to cart': r =>
+            !!r.addItemToOrder.lines.find(l => l.productVariant.id === variantId && l.quantity >= qty),
     });
 }