Browse Source

feat(server): Improved error handling

All errors now inherit from I18nError and each has a distinct error code, which makes client-side handling consistent. Closes #42
Michael Bromley 7 years ago
parent
commit
2e92fc123f
40 changed files with 200 additions and 151 deletions
  1. 9 0
      README.md
  2. 0 0
      schema.json
  3. 9 9
      server/e2e/auth.e2e-spec.ts
  4. 5 1
      server/e2e/order.e2e-spec.ts
  5. 1 1
      server/e2e/product.e2e-spec.ts
  6. 2 2
      server/src/api/common/request-context.service.ts
  7. 7 1
      server/src/api/middleware/auth-guard.ts
  8. 2 2
      server/src/api/resolvers/auth.resolver.ts
  9. 2 2
      server/src/api/resolvers/facet.resolver.ts
  10. 3 3
      server/src/api/resolvers/order.resolver.ts
  11. 2 5
      server/src/api/resolvers/product.resolver.ts
  12. 58 0
      server/src/common/error/errors.ts
  13. 2 2
      server/src/config/asset-preview-strategy/no-asset-preview-strategy.ts
  14. 7 7
      server/src/config/asset-storage-strategy/no-asset-storage-strategy.ts
  15. 2 3
      server/src/entity/payment-method/payment-method.entity.ts
  16. 3 3
      server/src/entity/product-variant/product-variant.subscriber.ts
  17. 10 4
      server/src/i18n/i18n-error.ts
  18. 3 0
      server/src/i18n/i18n.service.ts
  19. 3 2
      server/src/i18n/messages/en.json
  20. 2 2
      server/src/service/helpers/list-query-builder/parse-filter-params.ts
  21. 2 2
      server/src/service/helpers/list-query-builder/parse-sort-params.ts
  22. 5 6
      server/src/service/helpers/order-state-machine/order-state-machine.ts
  23. 2 2
      server/src/service/helpers/translatable-saver/translation-differ.ts
  24. 2 5
      server/src/service/helpers/utils/get-entity-or-throw.ts
  25. 2 2
      server/src/service/helpers/utils/translate-entity.ts
  26. 4 10
      server/src/service/services/administrator.service.ts
  27. 5 5
      server/src/service/services/auth.service.ts
  28. 4 7
      server/src/service/services/channel.service.ts
  29. 3 3
      server/src/service/services/customer.service.ts
  30. 9 12
      server/src/service/services/order.service.ts
  31. 2 2
      server/src/service/services/payment-method.service.ts
  32. 4 7
      server/src/service/services/product-variant.service.ts
  33. 3 6
      server/src/service/services/product.service.ts
  34. 3 6
      server/src/service/services/promotion.service.ts
  35. 5 5
      server/src/service/services/role.service.ts
  36. 4 7
      server/src/service/services/shipping-method.service.ts
  37. 2 5
      server/src/service/services/tax-category.service.ts
  38. 2 5
      server/src/service/services/tax-rate.service.ts
  39. 2 5
      server/yarn.lock
  40. 3 0
      shared/generated-types.ts

+ 9 - 0
README.md

@@ -96,6 +96,15 @@ This can be overridden by appending a `lang` query parameter to the url (e.g. `h
 
 All locales in Vendure are represented by 2-character [ISO 639-1 language codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
 
+Translations for localized strings are located in the [i18n/messages](./server/src/i18n/messages) directory.
+
+### Errors
+
+All errors thrown by the Vendure server can be found in the [errors.ts file](./server/src/common/error/errors.ts). 
+
+All errors extend from `I18nError`, which means that the error messages are localized as described above. Each error type
+has a distinct code which can be used by the front-end client in order to correctly handle the error.
+
 ### Custom Fields
 
 The developer may add custom fields to most of the entities in Vendure, which may contain any data specific to their

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 9 - 9
server/e2e/auth.e2e-spec.ts

@@ -217,7 +217,7 @@ describe('Authorization & permissions', () => {
                 await client.asUserWithCredentials(emailAddress, '');
                 fail('should have thrown');
             } catch (err) {
-                expect(getErrorStatusCode(err)).toBe(401);
+                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
             }
         });
 
@@ -268,26 +268,26 @@ describe('Authorization & permissions', () => {
             const status = await client.queryStatus(operation, variables);
             expect(status).toBe(200);
         } catch (e) {
-            const status = getErrorStatusCode(e);
-            if (!status) {
+            const errorCode = getErrorCode(e);
+            if (!errorCode) {
                 fail(`Unexpected failure: ${e}`);
             } else {
-                fail(`Operation should be allowed, got status ${getErrorStatusCode(e)}`);
+                fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
             }
         }
     }
 
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
         try {
-            const status = await client.queryStatus(operation, variables);
-            fail(`Should have thrown with 403 error, got ${status}`);
+            const status = await client.query(operation, variables);
+            fail(`Should have thrown`);
         } catch (e) {
-            expect(getErrorStatusCode(e)).toBe(403);
+            expect(getErrorCode(e)).toBe('FORBIDDEN');
         }
     }
 
-    function getErrorStatusCode(err: any): number {
-        return err.response.errors[0].message.statusCode;
+    function getErrorCode(err: any): string {
+        return err.response.errors[0].extensions.code;
     }
 
     async function createAdministratorWithPermissions(

+ 5 - 1
server/e2e/order.e2e-spec.ts

@@ -573,7 +573,11 @@ describe('Orders', () => {
                         });
                         fail('Should have thrown');
                     } catch (err) {
-                        expect(err.message).toEqual(expect.stringContaining(`This action is forbidden`));
+                        expect(err.message).toEqual(
+                            expect.stringContaining(
+                                `You are not currently authorized to perform this action`,
+                            ),
+                        );
                     }
                 });
             });

+ 1 - 1
server/e2e/product.e2e-spec.ts

@@ -383,7 +383,7 @@ describe('Product resolver', () => {
                 fail('Should have thrown');
             } catch (err) {
                 expect(err.message).toEqual(
-                    expect.stringContaining(`No OptionGroup with the id '999' could be found`),
+                    expect.stringContaining(`No ProductOptionGroup with the id '999' could be found`),
                 );
             }
         });

+ 2 - 2
server/src/api/common/request-context.service.ts

@@ -2,13 +2,13 @@ import { Injectable } from '@nestjs/common';
 import { Request } from 'express';
 import { LanguageCode, Permission } from 'shared/generated-types';
 
+import { NoValidChannelError } from '../../common/error/errors';
 import { idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { Channel } from '../../entity/channel/channel.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ChannelService } from '../../service/services/channel.service';
 
 import { RequestContext } from './request-context';
@@ -56,7 +56,7 @@ export class RequestContextService {
         } else if (req && req.headers && req.headers[tokenKey]) {
             channelToken = req.headers[tokenKey] as string;
         } else {
-            throw new I18nError('error.no-valid-channel-specified');
+            throw new NoValidChannelError();
         }
         return channelToken;
     }

+ 7 - 1
server/src/api/middleware/auth-guard.ts

@@ -8,6 +8,7 @@ import { ConfigService } from '../../config/config.service';
 import { Session } from '../../entity/session/session.entity';
 import { AuthService } from '../../service/services/auth.service';
 
+import { ForbiddenError } from '../../common/error/errors';
 import { extractAuthToken } from '../common/extract-auth-token';
 import { REQUEST_CONTEXT_KEY, RequestContextService } from '../common/request-context.service';
 import { setAuthToken } from '../common/set-auth-token';
@@ -43,7 +44,12 @@ export class AuthGuard implements CanActivate {
         if (authDisabled || !permissions || isPublic) {
             return true;
         } else {
-            return requestContext.isAuthorized || requestContext.authorizedAsOwnerOnly;
+            const canActivate = requestContext.isAuthorized || requestContext.authorizedAsOwnerOnly;
+            if (!canActivate) {
+                throw new ForbiddenError();
+            } else {
+                return canActivate;
+            }
         }
     }
 

+ 2 - 2
server/src/api/resolvers/auth.resolver.ts

@@ -9,9 +9,9 @@ import {
     VerifyCustomerAccountMutationArgs,
 } from 'shared/generated-types';
 
+import { VerificationTokenError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { AuthService } from '../../service/services/auth.service';
 import { ChannelService } from '../../service/services/channel.service';
 import { CustomerService } from '../../service/services/customer.service';
@@ -99,7 +99,7 @@ export class AuthResolver {
                 res,
             );
         } else {
-            throw new I18nError(`error.verification-token-not-recognized`);
+            throw new VerificationTokenError();
         }
     }
 

+ 2 - 2
server/src/api/resolvers/facet.resolver.ts

@@ -11,10 +11,10 @@ import {
 import { PaginatedList } from 'shared/shared-types';
 
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { Translated } from '../../common/types/locale-types';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { Facet } from '../../entity/facet/facet.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { FacetService } from '../../service/services/facet.service';
 import { RequestContext } from '../common/request-context';
@@ -74,7 +74,7 @@ export class FacetResolver {
         const facetId = input[0].facetId;
         const facet = await this.facetService.findOne(facetId, DEFAULT_LANGUAGE_CODE);
         if (!facet) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Facet', id: facetId });
+            throw new EntityNotFoundError('Facet', facetId);
         }
         return Promise.all(input.map(facetValue => this.facetValueService.create(facet, facetValue)));
     }

+ 3 - 3
server/src/api/resolvers/order.resolver.ts

@@ -17,8 +17,8 @@ import {
 } from 'shared/generated-types';
 import { PaginatedList } from 'shared/shared-types';
 
+import { ForbiddenError, InternalServerError } from '../../common/error/errors';
 import { Order } from '../../entity/order/order.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { OrderState } from '../../service/helpers/order-state-machine/order-state';
 import { AuthService } from '../../service/services/auth.service';
 import { CustomerService } from '../../service/services/customer.service';
@@ -122,7 +122,7 @@ export class OrderResolver {
             }
             // We throw even if the order does not exist, since giving a different response
             // opens the door to an enumeration attack to find valid order codes.
-            throw new I18nError(`error.forbidden`);
+            throw new ForbiddenError();
         }
     }
 
@@ -264,7 +264,7 @@ export class OrderResolver {
         createIfNotExists = false,
     ): Promise<Order | undefined> {
         if (!ctx.session) {
-            throw new I18nError(`error.no-active-session`);
+            throw new InternalServerError(`error.no-active-session`);
         }
         let order = ctx.session.activeOrder;
         if (!order) {

+ 2 - 5
server/src/api/resolvers/product.resolver.ts

@@ -14,11 +14,11 @@ import {
 import { ID, PaginatedList } from 'shared/shared-types';
 
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { ProductVariantService } from '../../service/services/product-variant.service';
 import { ProductService } from '../../service/services/product.service';
@@ -149,10 +149,7 @@ export class ProductResolver {
             (facetValueIds as ID[]).map(async facetValueId => {
                 const facetValue = await this.facetValueService.findOne(facetValueId, DEFAULT_LANGUAGE_CODE);
                 if (!facetValue) {
-                    throw new I18nError('error.entity-with-id-not-found', {
-                        entityName: 'FacetValue',
-                        id: facetValueId,
-                    });
+                    throw new EntityNotFoundError('FacetValue', facetValueId);
                 }
                 return facetValue;
             }),

+ 58 - 0
server/src/common/error/errors.ts

@@ -0,0 +1,58 @@
+import { ID } from 'shared/shared-types';
+
+import { coreEntitiesMap } from '../../entity/entities';
+import { I18nError } from '../../i18n/i18n-error';
+
+export class InternalServerError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message, variables, 'INTERNAL_SERVER_ERROR');
+    }
+}
+
+export class UserInputError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message, variables, 'USER_INPUT_ERROR');
+    }
+}
+
+export class IllegalOperationError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message || 'error.cannot-transition-order-from-to', variables, 'ILLEGAL_OPERATION');
+    }
+}
+
+export class UnauthorizedError extends I18nError {
+    constructor() {
+        super('error.unauthorized', {}, 'UNAUTHORIZED');
+    }
+}
+
+export class ForbiddenError extends I18nError {
+    constructor() {
+        super('error.forbidden', {}, 'FORBIDDEN');
+    }
+}
+
+export class NoValidChannelError extends I18nError {
+    constructor() {
+        super('error.no-valid-channel-specified', {}, 'NO_VALID_CHANNEL');
+    }
+}
+
+export class EntityNotFoundError extends I18nError {
+    constructor(entityName: keyof typeof coreEntitiesMap, id: ID) {
+        super('error.entity-with-id-not-found', { entityName, id }, 'ENTITY_NOT_FOUND');
+    }
+}
+
+export class VerificationTokenError extends I18nError {
+    constructor() {
+        super('error.verification-token-not-recognized', {}, 'BAD_VERIFICATION_TOKEN');
+    }
+}
+
+export class NotVerifiedError extends I18nError {
+    constructor() {
+        super('error.email-address-not-verified', {}, 'NOT_VERIFIED');
+    }
+}

+ 2 - 2
server/src/config/asset-preview-strategy/no-asset-preview-strategy.ts

@@ -1,4 +1,4 @@
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 import { AssetPreviewStrategy } from './asset-preview-strategy';
 
@@ -7,6 +7,6 @@ import { AssetPreviewStrategy } from './asset-preview-strategy';
  */
 export class NoAssetPreviewStrategy implements AssetPreviewStrategy {
     generatePreviewImage(mimeType: string, data: Buffer): Promise<Buffer> {
-        throw new I18nError('error.no-asset-preview-strategy-configured');
+        throw new InternalServerError('error.no-asset-preview-strategy-configured');
     }
 }

+ 7 - 7
server/src/config/asset-storage-strategy/no-asset-storage-strategy.ts

@@ -1,7 +1,7 @@
 import { Request } from 'express';
 import { Stream } from 'stream';
 
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 import { AssetStorageStrategy } from './asset-storage-strategy';
 
@@ -12,26 +12,26 @@ const errorMessage = 'error.no-asset-storage-strategy-configured';
  */
 export class NoAssetStorageStrategy implements AssetStorageStrategy {
     writeFileFromStream(fileName: string, data: Stream): Promise<string> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 
     writeFileFromBuffer(fileName: string, data: Buffer): Promise<string> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 
     readFileToBuffer(identifier: string): Promise<Buffer> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 
     readFileToStream(identifier: string): Promise<Stream> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 
     toAbsoluteUrl(request: Request, identifier: string): string {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 
     fileExists(fileName: string): Promise<boolean> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
 }

+ 2 - 3
server/src/entity/payment-method/payment-method.entity.ts

@@ -2,9 +2,8 @@ import { ConfigArg } from 'shared/generated-types';
 import { DeepPartial } from 'shared/shared-types';
 import { Column, Entity } from 'typeorm';
 
-import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
+import { UserInputError } from '../../common/error/errors';
 import { getConfig } from '../../config/vendure-config';
-import { I18nError } from '../../i18n/i18n-error';
 import { VendureEntity } from '../base/base.entity';
 import { Order } from '../order/order.entity';
 import { Payment, PaymentMetadata } from '../payment/payment.entity';
@@ -24,7 +23,7 @@ export class PaymentMethod extends VendureEntity {
     async createPayment(order: Order, metadata: PaymentMetadata) {
         const handler = getConfig().paymentOptions.paymentMethodHandlers.find(h => h.code === this.code);
         if (!handler) {
-            throw new I18nError(`error.no-payment-handler-with-code`, { code: this.code });
+            throw new UserInputError(`error.no-payment-handler-with-code`, { code: this.code });
         }
         const result = await handler.createPayment(order, this.configArgs, metadata || {});
         return new Payment(result);

+ 3 - 3
server/src/entity/product-variant/product-variant.subscriber.ts

@@ -1,6 +1,6 @@
 import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
 
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 import { ProductVariantPrice } from './product-variant-price.entity';
 import { ProductVariant } from './product-variant.entity';
@@ -18,7 +18,7 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ
         const { channelId, taxCategoryId } = event.queryRunner.data;
         const price = event.entity.price || 0;
         if (channelId === undefined) {
-            throw new I18nError(`error.channel-id-not-set`);
+            throw new InternalServerError(`error.channel-id-not-set`);
         }
         const variantPrice = new ProductVariantPrice({ price, channelId });
         variantPrice.variant = event.entity;
@@ -33,7 +33,7 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ
             },
         });
         if (!variantPrice) {
-            throw new I18nError(`error.could-not-find-product-variant-price`);
+            throw new InternalServerError(`error.could-not-find-product-variant-price`);
         }
 
         variantPrice.price = event.entity.price || 0;

+ 10 - 4
server/src/i18n/i18n-error.ts

@@ -1,5 +1,7 @@
+import { ApolloError } from 'apollo-server-core';
+
 /**
- * All errors thrown in the Vendure server must use this error class. This allows the
+ * All errors thrown in the Vendure server must use or extend this error class. This allows the
  * error message to be translated before being served to the client.
  *
  * The message should be of the form `Could not find user {{ id }}`, with the variables argument
@@ -10,8 +12,12 @@
  * throw new I18nError(`Could not find user {{ id }}`, { id });
  * ```
  */
-export class I18nError extends Error {
-    constructor(public message: string, public variables: { [key: string]: string | number } = {}) {
-        super(message);
+export abstract class I18nError extends ApolloError {
+    protected constructor(
+        public message: string,
+        public variables: { [key: string]: string | number } = {},
+        code?: string,
+    ) {
+        super(message, code);
     }
 }

+ 3 - 0
server/src/i18n/i18n.service.ts

@@ -63,6 +63,9 @@ export class I18nService {
                 translation += ` (Translation format error: ${e.message})`;
             }
             error.message = translation;
+            // We can now safely remove the variables object so that they do not appear in
+            // the error returned by the GraphQL API
+            delete originalError.variables;
         }
 
         return error;

+ 3 - 2
server/src/i18n/messages/en.json

@@ -8,13 +8,14 @@
     "email-address-not-verified": "Please verify this email address before logging in",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
-    "forbidden": "This action is forbidden",
+    "forbidden": "You are not currently authorized to perform this action",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
     "no-valid-channel-specified": "No valid channel was specified",
     "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
     "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem",
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
-    "verification-token-not-recognized": "Verification token not recognized"
+    "verification-token-not-recognized": "Verification token not recognized",
+    "unauthorized": "The credentials did not match. Please check and try again"
   }
 }

+ 2 - 2
server/src/service/helpers/list-query-builder/parse-filter-params.ts

@@ -4,8 +4,8 @@ import { Connection } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
+import { UserInputError } from '../../../common/error/errors';
 import {
     BooleanOperators,
     DateOperators,
@@ -55,7 +55,7 @@ export function parseFilterParams<T extends VendureEntity>(
                 } else if (translationColumns.find(c => c.propertyName === key)) {
                     fieldName = `${alias}_translations.${key}`;
                 } else {
-                    throw new I18nError('error.invalid-filter-field');
+                    throw new UserInputError('error.invalid-filter-field');
                 }
                 const condition = buildWhereCondition(fieldName, operator as Operator, operand);
                 output.push(condition);

+ 2 - 2
server/src/service/helpers/list-query-builder/parse-sort-params.ts

@@ -3,8 +3,8 @@ import { Connection, OrderByCondition } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
+import { UserInputError } from '../../../common/error/errors';
 import { NullOptionals, SortParameter } from '../../../common/types/common-types';
 
 /**
@@ -43,7 +43,7 @@ export function parseSortParams<T extends VendureEntity>(
         } else if (translationColumns.find(c => c.propertyName === key)) {
             output[`${alias}_translations.${key}`] = order;
         } else {
-            throw new I18nError('error.invalid-sort-field', {
+            throw new UserInputError('error.invalid-sort-field', {
                 fieldName: key,
                 validFields: getValidSortFields([...columns, ...translationColumns]),
             });

+ 5 - 6
server/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -1,12 +1,12 @@
 import { Injectable } from '@nestjs/common';
 
 import { RequestContext } from '../../../api/common/request-context';
+import { IllegalOperationError } from '../../../common/error/errors';
 import { FSM, StateMachineConfig, Transitions } from '../../../common/finite-state-machine';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { EventBus } from '../../../event-bus/event-bus';
 import { OrderStateTransitionEvent } from '../../../event-bus/events/order-state-transition-event';
-import { I18nError } from '../../../i18n/i18n-error';
 
 import { OrderState, orderStateTransitions, OrderTransitionData } from './order-state';
 
@@ -91,11 +91,10 @@ export class OrderStateMachine {
                 if (typeof onError === 'function') {
                     onError(fromState, toState, message);
                 }
-                if (!message) {
-                    throw new I18nError(`error.cannot-transition-order-from-to`, { fromState, toState });
-                } else {
-                    throw new I18nError(message);
-                }
+                throw new IllegalOperationError(message || 'error.cannot-transition-order-from-to', {
+                    fromState,
+                    toState,
+                });
             },
         };
     }

+ 2 - 2
server/src/service/helpers/translatable-saver/translation-differ.ts

@@ -1,9 +1,9 @@
 import { DeepPartial } from 'shared/shared-types';
 import { EntityManager } from 'typeorm';
 
+import { EntityNotFoundError } from '../../../common/error/errors';
 import { Translatable, Translation, TranslationInput } from '../../../common/types/locale-types';
 import { foundIn, not } from '../../../common/utils';
-import { I18nError } from '../../../i18n/i18n-error';
 
 export interface TranslationContructor<T> {
     new (input?: DeepPartial<TranslationInput<T>> | DeepPartial<Translation<T>>): Translation<T>;
@@ -70,7 +70,7 @@ export class TranslationDiffer<Entity extends Translatable> {
                 } catch (err) {
                     const entityName = entity.constructor.name;
                     const id = (entity as any).id || 'undefined';
-                    throw new I18nError('error.entity-with-id-not-found', { entityName, id });
+                    throw new EntityNotFoundError(entityName as any, id);
                 }
                 entity.translations.push(newTranslation);
             }

+ 2 - 5
server/src/service/helpers/utils/get-entity-or-throw.ts

@@ -1,8 +1,8 @@
 import { ID, Type } from 'shared/shared-types';
 import { Connection, FindOneOptions } from 'typeorm';
 
+import { EntityNotFoundError } from '../../../common/error/errors';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
 /**
  * Attempts to find an entity of the given type and id, and throws an error if not found.
@@ -15,10 +15,7 @@ export async function getEntityOrThrow<T extends VendureEntity>(
 ): Promise<T> {
     const entity = await connection.getRepository(entityType).findOne(id, findOptions);
     if (!entity) {
-        throw new I18nError(`error.entity-with-id-not-found`, {
-            entityName: entityType.name,
-            id,
-        });
+        throw new EntityNotFoundError(entityType.name as any, id);
     }
     return entity;
 }

+ 2 - 2
server/src/service/helpers/utils/translate-entity.ts

@@ -1,8 +1,8 @@
 import { LanguageCode } from 'shared/generated-types';
 
 import { UnwrappedArray } from '../../../common/types/common-types';
-import { I18nError } from '../../../i18n/i18n-error';
 
+import { InternalServerError } from '../../../common/error/errors';
 import { Translatable, Translated } from '../../../common/types/locale-types';
 
 // prettier-ignore
@@ -42,7 +42,7 @@ export function translateEntity<T extends Translatable>(
         translatable.translations && translatable.translations.find(t => t.languageCode === languageCode);
 
     if (!translation) {
-        throw new I18nError(`error.entity-has-no-translation-in-language`, {
+        throw new InternalServerError(`error.entity-has-no-translation-in-language`, {
             entityName: translatable.constructor.name,
             languageCode,
         });

+ 4 - 10
server/src/service/services/administrator.service.ts

@@ -5,10 +5,10 @@ import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from 'shared/s
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -59,10 +59,7 @@ export class AdministratorService {
     async update(input: UpdateAdministratorInput): Promise<Administrator> {
         const administrator = await this.findOne(input.id);
         if (!administrator) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'Administrator',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Administrator', input.id);
         }
         let updatedAdministrator = patchEntity(administrator, input);
         await this.connection.manager.save(administrator);
@@ -86,14 +83,11 @@ export class AdministratorService {
     async assignRole(administratorId: ID, roleId: ID): Promise<Administrator> {
         const administrator = await this.findOne(administratorId);
         if (!administrator) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                id: administratorId,
-                entityName: 'Administrator',
-            });
+            throw new EntityNotFoundError('Administrator', administratorId);
         }
         const role = await this.roleService.findOne(roleId);
         if (!role) {
-            throw new I18nError(`error.entity-with-id-not-found`, { id: roleId, entityName: 'Role' });
+            throw new EntityNotFoundError('Role', roleId);
         }
         administrator.user.roles.push(role);
         await this.connection.manager.save(administrator.user);

+ 5 - 5
server/src/service/services/auth.service.ts

@@ -1,17 +1,17 @@
-import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import * as crypto from 'crypto';
 import * as ms from 'ms';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { NotVerifiedError, UnauthorizedError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { Order } from '../../entity/order/order.entity';
 import { AnonymousSession } from '../../entity/session/anonymous-session.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 
 import { OrderService } from './order.service';
@@ -43,10 +43,10 @@ export class AuthService {
         const user = await this.getUserFromIdentifier(identifier);
         const passwordMatches = await this.passwordCipher.check(password, user.passwordHash);
         if (!passwordMatches) {
-            throw new UnauthorizedException();
+            throw new UnauthorizedError();
         }
         if (this.configService.authOptions.requireVerification && !user.verified) {
-            throw new I18nError(`error.email-address-not-verified`);
+            throw new NotVerifiedError();
         }
         await this.deleteSessionsByUser(user);
         if (ctx.session && ctx.session.activeOrder) {
@@ -153,7 +153,7 @@ export class AuthService {
             relations: ['roles', 'roles.channels'],
         });
         if (!user) {
-            throw new UnauthorizedException();
+            throw new UnauthorizedError();
         }
         return user;
     }

+ 4 - 7
server/src/service/services/channel.service.ts

@@ -7,12 +7,12 @@ import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ChannelAware } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Zone } from '../../entity/zone/zone.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -47,7 +47,7 @@ export class ChannelService {
     getChannelFromToken(token: string): Channel {
         const channel = this.allChannels.find(c => c.token === token);
         if (!channel) {
-            throw new I18nError(`error.channel-not-found`, { token });
+            throw new InternalServerError(`error.channel-not-found`, { token });
         }
         return channel;
     }
@@ -59,7 +59,7 @@ export class ChannelService {
         const defaultChannel = this.allChannels.find(channel => channel.code === DEFAULT_CHANNEL_CODE);
 
         if (!defaultChannel) {
-            throw new I18nError(`error.default-channel-not-found`);
+            throw new InternalServerError(`error.default-channel-not-found`);
         }
         return defaultChannel;
     }
@@ -96,10 +96,7 @@ export class ChannelService {
     async update(input: UpdateChannelInput): Promise<Channel> {
         const channel = await this.findOne(input.id);
         if (!channel) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'Channel',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Channel', input.id);
         }
         const updatedChannel = patchEntity(channel, input);
         if (input.defaultTaxZoneId) {

+ 3 - 3
server/src/service/services/customer.service.ts

@@ -11,13 +11,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, normalizeEmailAddress } from '../../common/utils';
 import { Address } from '../../entity/address/address.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -71,7 +71,7 @@ export class CustomerService {
         });
 
         if (existing) {
-            throw new I18nError(`error.email-address-must-be-unique`);
+            throw new InternalServerError(`error.email-address-must-be-unique`);
         }
 
         if (password) {
@@ -155,7 +155,7 @@ export class CustomerService {
         });
 
         if (!customer) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Customer', id: customerId });
+            throw new EntityNotFoundError('Customer', customerId);
         }
 
         const address = new Address(input);

+ 9 - 12
server/src/service/services/order.service.ts

@@ -4,6 +4,7 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, IllegalOperationError, UserInputError } from '../../common/error/errors';
 import { generatePublicId } from '../../common/generate-public-id';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { idsAreEqual } from '../../common/utils';
@@ -15,7 +16,6 @@ import { Payment } from '../../entity/payment/payment.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
@@ -219,7 +219,7 @@ export class OrderService {
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const selectedMethod = eligibleMethods.find(m => idsAreEqual(m.method.id, shippingMethodId));
         if (!selectedMethod) {
-            throw new I18nError(`error.shipping-method-unavailable`);
+            throw new UserInputError(`error.shipping-method-unavailable`);
         }
         order.shippingMethod = selectedMethod.method;
         await this.connection.getRepository(Order).save(order);
@@ -237,7 +237,7 @@ export class OrderService {
     async addPaymentToOrder(ctx: RequestContext, orderId: ID, input: PaymentInput): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.state !== 'ArrangingPayment') {
-            throw new I18nError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
+            throw new IllegalOperationError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
         }
         const payment = await this.paymentMethodService.createPayment(order, input.method, input.metadata);
         if (order.payments) {
@@ -259,7 +259,7 @@ export class OrderService {
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {
-            throw new I18nError(`error.order-already-has-customer`);
+            throw new IllegalOperationError(`error.order-already-has-customer`);
         }
         order.customer = customer;
         return this.connection.getRepository(Order).save(order);
@@ -297,7 +297,7 @@ export class OrderService {
     private async getOrderOrThrow(ctx: RequestContext, orderId: ID): Promise<Order> {
         const order = await this.findOne(ctx, orderId);
         if (!order) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Order', id: orderId });
+            throw new EntityNotFoundError('Order', orderId);
         }
         return order;
     }
@@ -308,10 +308,7 @@ export class OrderService {
     ): Promise<ProductVariant> {
         const productVariant = await this.productVariantService.findOne(ctx, productVariantId);
         if (!productVariant) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'ProductVariant',
-                id: productVariantId,
-            });
+            throw new EntityNotFoundError('ProductVariant', productVariantId);
         }
         return productVariant;
     }
@@ -319,7 +316,7 @@ export class OrderService {
     private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
         const orderItem = order.lines.find(line => idsAreEqual(line.id, orderLineId));
         if (!orderItem) {
-            throw new I18nError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
+            throw new UserInputError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
         }
         return orderItem;
     }
@@ -337,7 +334,7 @@ export class OrderService {
      */
     private assertQuantityIsPositive(quantity: number) {
         if (quantity < 0) {
-            throw new I18nError(`error.order-item-quantity-must-be-positive`, { quantity });
+            throw new IllegalOperationError(`error.order-item-quantity-must-be-positive`, { quantity });
         }
     }
 
@@ -346,7 +343,7 @@ export class OrderService {
      */
     private assertAddingItemsState(order: Order) {
         if (order.state !== 'AddingItems') {
-            throw new I18nError(`error.order-contents-may-only-be-modified-in-addingitems-state`);
+            throw new IllegalOperationError(`error.order-contents-may-only-be-modified-in-addingitems-state`);
         }
     }
 

+ 2 - 2
server/src/service/services/payment-method.service.ts

@@ -6,6 +6,7 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { assertNever } from 'shared/shared-utils';
 import { Connection } from 'typeorm';
 
+import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ConfigService } from '../../config/config.service';
 import {
@@ -16,7 +17,6 @@ import {
 import { Order } from '../../entity/order/order.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -73,7 +73,7 @@ export class PaymentMethodService {
             },
         });
         if (!paymentMethod) {
-            throw new I18nError(`error.payment-method-not-found`, { method });
+            throw new UserInputError(`error.payment-method-not-found`, { method });
         }
         const payment = await paymentMethod.createPayment(order, metadata);
         return this.connection.getRepository(Payment).save(payment);

+ 4 - 7
server/src/service/services/product-variant.service.ts

@@ -7,6 +7,7 @@ import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
@@ -14,7 +15,6 @@ import { ProductOption } from '../../entity/product-option/product-option.entity
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -128,7 +128,7 @@ export class ProductVariantService {
         });
 
         if (!product) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Product', id: productId });
+            throw new EntityNotFoundError('Product', productId);
         }
         const defaultTranslation = product.translations.find(t => t.languageCode === DEFAULT_LANGUAGE_CODE);
 
@@ -172,10 +172,7 @@ export class ProductVariantService {
 
         const notFoundIds = productVariantIds.filter(id => !variants.find(v => idsAreEqual(v.id, id)));
         if (notFoundIds.length) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'ProductVariant',
-                id: notFoundIds[0],
-            });
+            throw new EntityNotFoundError('ProductVariant', notFoundIds[0]);
         }
         for (const variant of variants) {
             for (const facetValue of facetValues) {
@@ -200,7 +197,7 @@ export class ProductVariantService {
     applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): ProductVariant {
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         if (!channelPrice) {
-            throw new I18nError(`error.no-price-found-for-channel`);
+            throw new InternalServerError(`error.no-price-found-for-channel`);
         }
         const applicableTaxRate = this.taxRateService.getApplicableTaxRate(
             ctx.activeTaxZone,

+ 3 - 6
server/src/service/services/product.service.ts

@@ -5,13 +5,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductTranslation } from '../../entity/product/product-translation.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -93,10 +93,7 @@ export class ProductService {
         const product = await this.getProductWithOptionGroups(productId);
         const optionGroup = await this.connection.getRepository(ProductOptionGroup).findOne(optionGroupId);
         if (!optionGroup) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'OptionGroup',
-                id: optionGroupId,
-            });
+            throw new EntityNotFoundError('ProductOptionGroup', optionGroupId);
         }
 
         if (Array.isArray(product.optionGroups)) {
@@ -142,7 +139,7 @@ export class ProductService {
             .getRepository(Product)
             .findOne(productId, { relations: ['optionGroups'] });
         if (!product) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Product', id: productId });
+            throw new EntityNotFoundError('Product', productId);
         }
         return product;
     }

+ 3 - 6
server/src/service/services/promotion.service.ts

@@ -11,13 +11,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { PromotionAction } from '../../config/promotion/promotion-action';
 import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { Promotion } from '../../entity/promotion/promotion.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -105,10 +105,7 @@ export class PromotionService {
     async updatePromotion(ctx: RequestContext, input: UpdatePromotionInput): Promise<Promotion> {
         const adjustmentSource = await this.connection.getRepository(Promotion).findOne(input.id);
         if (!adjustmentSource) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'AdjustmentSource',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Promotion', input.id);
         }
         const updatedAdjustmentSource = patchEntity(adjustmentSource, omit(input, ['conditions', 'actions']));
         if (input.conditions) {
@@ -165,7 +162,7 @@ export class PromotionService {
             type === 'condition' ? this.availableConditions : this.availableActions;
         const match = available.find(a => a.code === code);
         if (!match) {
-            throw new I18nError(`error.adjustment-operation-with-code-not-found`, { code });
+            throw new UserInputError(`error.adjustment-operation-with-code-not-found`, { code });
         }
         return match;
     }

+ 5 - 5
server/src/service/services/role.service.ts

@@ -10,10 +10,10 @@ import {
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { Role } from '../../entity/role/role.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -51,7 +51,7 @@ export class RoleService {
     getSuperAdminRole(): Promise<Role> {
         return this.getRoleByCode(SUPER_ADMIN_ROLE_CODE).then(role => {
             if (!role) {
-                throw new I18nError(`error.super-admin-role-not-found`);
+                throw new InternalServerError(`error.super-admin-role-not-found`);
             }
             return role;
         });
@@ -60,7 +60,7 @@ export class RoleService {
     getCustomerRole(): Promise<Role> {
         return this.getRoleByCode(CUSTOMER_ROLE_CODE).then(role => {
             if (!role) {
-                throw new I18nError(`error.customer-role-not-found`);
+                throw new InternalServerError(`error.customer-role-not-found`);
             }
             return role;
         });
@@ -75,10 +75,10 @@ export class RoleService {
     async update(input: UpdateRoleInput): Promise<Role> {
         const role = await this.findOne(input.id);
         if (!role) {
-            throw new I18nError(`error.entity-with-id-not-found`, { entityName: 'Role', id: input.id });
+            throw new EntityNotFoundError('Role', input.id);
         }
         if (role.code === SUPER_ADMIN_ROLE_CODE || role.code === CUSTOMER_ROLE_CODE) {
-            throw new I18nError(`error.cannot-modify-role`, { roleCode: role.code });
+            throw new InternalServerError(`error.cannot-modify-role`, { roleCode: role.code });
         }
         const updatedRole = patchEntity(role, input);
         await this.connection.manager.save(updatedRole);

+ 4 - 7
server/src/service/services/shipping-method.service.ts

@@ -10,6 +10,7 @@ import { omit } from 'shared/omit';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -17,7 +18,6 @@ import { ShippingCalculator } from '../../config/shipping-method/shipping-calcul
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { Channel } from '../../entity/channel/channel.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -76,10 +76,7 @@ export class ShippingMethodService {
     async update(input: UpdateShippingMethodInput): Promise<ShippingMethod> {
         const shippingMethod = await this.findOne(input.id);
         if (!shippingMethod) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'ShippingMethod',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('ShippingMethod', input.id);
         }
         const updatedShippingMethod = patchEntity(shippingMethod, omit(input, ['checker', 'calculator']));
         if (input.checker) {
@@ -143,7 +140,7 @@ export class ShippingMethodService {
     private getChecker(code: string): ShippingEligibilityChecker {
         const match = this.shippingEligibilityCheckers.find(a => a.code === code);
         if (!match) {
-            throw new I18nError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
+            throw new UserInputError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
         }
         return match;
     }
@@ -151,7 +148,7 @@ export class ShippingMethodService {
     private getCalculator(code: string): ShippingCalculator {
         const match = this.shippingCalculators.find(a => a.code === code);
         if (!match) {
-            throw new I18nError(`error.shipping-calculator-with-code-not-found`, { code });
+            throw new UserInputError(`error.shipping-calculator-with-code-not-found`, { code });
         }
         return match;
     }

+ 2 - 5
server/src/service/services/tax-category.service.ts

@@ -4,9 +4,9 @@ import { CreateTaxCategoryInput, UpdateTaxCategoryInput } from 'shared/generated
 import { ID } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { assertFound } from '../../common/utils';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
@@ -30,10 +30,7 @@ export class TaxCategoryService {
     async update(input: UpdateTaxCategoryInput): Promise<TaxCategory> {
         const taxCategory = await this.findOne(input.id);
         if (!taxCategory) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'TaxCategory',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('TaxCategory', input.id);
         }
         const updatedTaxCategory = patchEntity(taxCategory, input);
         await this.connection.getRepository(TaxCategory).save(updatedTaxCategory);

+ 2 - 5
server/src/service/services/tax-rate.service.ts

@@ -3,13 +3,13 @@ import { CreateTaxRateInput, UpdateTaxRateInput } from 'shared/generated-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -71,10 +71,7 @@ export class TaxRateService {
     async update(input: UpdateTaxRateInput): Promise<TaxRate> {
         const taxRate = await this.findOne(input.id);
         if (!taxRate) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'TaxRate',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('TaxRate', input.id);
         }
         const updatedTaxRate = patchEntity(taxRate, input);
         if (input.categoryId) {

+ 2 - 5
server/yarn.lock

@@ -239,15 +239,12 @@
 "@types/i18next-express-middleware@^0.0.33":
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/@types/i18next-express-middleware/-/i18next-express-middleware-0.0.33.tgz#1c5625f123eaae126de3b43626ef9a04bc6ad482"
+  integrity sha512-lXuok/3HcT81FipU00+5W1cRjSpl5y0ImQKV2wxCaRkLfANMBi+T7idOVdhZutsX0OlnR2YxLM1I/NuTvYetoQ==
   dependencies:
     "@types/express" "*"
     "@types/i18next" "*"
 
-"@types/i18next@*":
-  version "8.4.4"
-  resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-8.4.4.tgz#76591ab5974b65910e44b8860202d4f544f39b9d"
-
-"@types/i18next@^11.9.3":
+"@types/i18next@*", "@types/i18next@^11.9.3":
   version "11.9.3"
   resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-11.9.3.tgz#04d84c6539908ad69665d26d8967f942d1638550"
   integrity sha512-snM7bMKy6gt7UYdpjsxycqSCAy0fr2JVPY0B8tJ2vp9bN58cE7C880k20PWFM4KXxQ3KsstKM8DLCawGCIH0tg==

+ 3 - 0
shared/generated-types.ts

@@ -85,6 +85,7 @@ export interface Query {
     taxRate?: TaxRate | null;
     zones: Zone[];
     zone?: Zone | null;
+    temp__?: boolean | null;
     networkStatus: NetworkStatus;
     userStatus: UserStatus;
     uiState: UiState;
@@ -1883,6 +1884,7 @@ export namespace QueryResolvers {
         taxRate?: TaxRateResolver<TaxRate | null, any, Context>;
         zones?: ZonesResolver<Zone[], any, Context>;
         zone?: ZoneResolver<Zone | null, any, Context>;
+        temp__?: TempResolver<boolean | null, any, Context>;
         networkStatus?: NetworkStatusResolver<NetworkStatus, any, Context>;
         userStatus?: UserStatusResolver<UserStatus, any, Context>;
         uiState?: UiStateResolver<UiState, any, Context>;
@@ -2266,6 +2268,7 @@ export namespace QueryResolvers {
         id: string;
     }
 
+    export type TempResolver<R = boolean | null, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type NetworkStatusResolver<R = NetworkStatus, Parent = any, Context = any> = Resolver<
         R,
         Parent,

Some files were not shown because too many files changed in this diff