Browse Source

fix(core): Limit Channel defaultLanguage to one of availableLanguages

Michael Bromley 5 years ago
parent
commit
b9f4dc0b04

+ 73 - 14
packages/core/e2e/channel.e2e-spec.ts

@@ -22,6 +22,8 @@ import {
     Me,
     Permission,
     RemoveProductsFromChannel,
+    UpdateChannel,
+    UpdateGlobalSettings,
 } from './graphql/generated-e2e-admin-types';
 import {
     ASSIGN_PRODUCT_TO_CHANNEL,
@@ -100,8 +102,8 @@ describe('Channels', () => {
 
         expect(me!.channels.length).toBe(2);
 
-        const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN);
-        const nonOwnerPermissions = Object.values(Permission).filter(p => p !== Permission.Owner);
+        const secondChannelData = me!.channels.find((c) => c.token === SECOND_CHANNEL_TOKEN);
+        const nonOwnerPermissions = Object.values(Permission).filter((p) => p !== Permission.Owner);
         expect(secondChannelData!.permissions).toEqual(nonOwnerPermissions);
     });
 
@@ -111,7 +113,7 @@ describe('Channels', () => {
 
         expect(me!.channels.length).toBe(2);
 
-        const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN);
+        const secondChannelData = me!.channels.find((c) => c.token === SECOND_CHANNEL_TOKEN);
         expect(me!.channels).toEqual([
             {
                 code: DEFAULT_CHANNEL_CODE,
@@ -170,7 +172,7 @@ describe('Channels', () => {
             },
         });
 
-        expect(createAdministrator.user.roles.map(r => r.description)).toEqual(['second channel admin']);
+        expect(createAdministrator.user.roles.map((r) => r.description)).toEqual(['second channel admin']);
     });
 
     it(
@@ -288,7 +290,7 @@ describe('Channels', () => {
                 },
             });
 
-            expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
+            expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']);
             await adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
             const { product } = await adminClient.query<
                 GetProductWithVariants.Query,
@@ -297,12 +299,12 @@ describe('Channels', () => {
                 id: product1.id,
             });
 
-            expect(product!.variants.map(v => v.price)).toEqual(
-                product1.variants.map(v => v.price * PRICE_FACTOR),
+            expect(product!.variants.map((v) => v.price)).toEqual(
+                product1.variants.map((v) => v.price * PRICE_FACTOR),
             );
             // Second Channel is configured to include taxes in price, so they should be the same.
-            expect(product!.variants.map(v => v.priceWithTax)).toEqual(
-                product1.variants.map(v => v.price * PRICE_FACTOR),
+            expect(product!.variants.map((v) => v.priceWithTax)).toEqual(
+                product1.variants.map((v) => v.price * PRICE_FACTOR),
             );
         });
 
@@ -317,7 +319,7 @@ describe('Channels', () => {
                 },
             });
 
-            expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
+            expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']);
         });
 
         it(
@@ -348,7 +350,44 @@ describe('Channels', () => {
                 },
             });
 
-            expect(removeProductsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
+            expect(removeProductsFromChannel[0].channels.map((c) => c.id)).toEqual(['T_1']);
+        });
+    });
+
+    describe('setting defaultLanguage', () => {
+        it(
+            'throws if languageCode not in availableLanguages',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
+                    input: {
+                        id: 'T_1',
+                        defaultLanguageCode: LanguageCode.zh,
+                    },
+                });
+            }, 'Language "zh" is not available. First enable it via GlobalSettings and try again.'),
+        );
+
+        it('allows setting to an available language', async () => {
+            await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
+                UPDATE_GLOBAL_SETTINGS,
+                {
+                    input: {
+                        availableLanguages: [LanguageCode.en, LanguageCode.zh],
+                    },
+                },
+            );
+
+            const { updateChannel } = await adminClient.query<
+                UpdateChannel.Mutation,
+                UpdateChannel.Variables
+            >(UPDATE_CHANNEL, {
+                input: {
+                    id: 'T_1',
+                    defaultLanguageCode: LanguageCode.zh,
+                },
+            });
+
+            expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
         });
     });
 
@@ -364,7 +403,7 @@ describe('Channels', () => {
                 productIds: [PROD_ID],
             },
         });
-        expect(assignProductsToChannel[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']);
+        expect(assignProductsToChannel[0].channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_2']);
 
         const { deleteChannel } = await adminClient.query<DeleteChannel.Mutation, DeleteChannel.Variables>(
             DELETE_CHANNEL,
@@ -376,7 +415,7 @@ describe('Channels', () => {
         expect(deleteChannel.result).toBe(DeletionResult.DELETED);
 
         const { channels } = await adminClient.query<GetChannels.Query>(GET_CHANNELS);
-        expect(channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']);
+        expect(channels.map((c) => c.id).sort()).toEqual(['T_1', 'T_3']);
 
         const { product } = await adminClient.query<
             GetProductWithVariants.Query,
@@ -384,7 +423,7 @@ describe('Channels', () => {
         >(GET_PRODUCT_WITH_VARIANTS, {
             id: PROD_ID,
         });
-        expect(product!.channels.map(c => c.id)).toEqual(['T_1']);
+        expect(product!.channels.map((c) => c.id)).toEqual(['T_1']);
     });
 });
 
@@ -398,6 +437,17 @@ const GET_CHANNELS = gql`
     }
 `;
 
+const UPDATE_CHANNEL = gql`
+    mutation UpdateChannel($input: UpdateChannelInput!) {
+        updateChannel(input: $input) {
+            id
+            code
+            defaultLanguageCode
+            currencyCode
+        }
+    }
+`;
+
 const DELETE_CHANNEL = gql`
     mutation DeleteChannel($id: ID!) {
         deleteChannel(id: $id) {
@@ -406,3 +456,12 @@ const DELETE_CHANNEL = gql`
         }
     }
 `;
+
+const UPDATE_GLOBAL_SETTINGS = gql`
+    mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
+        updateGlobalSettings(input: $input) {
+            id
+            availableLanguages
+        }
+    }
+`;

+ 34 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -3585,6 +3585,17 @@ export type GetChannelsQuery = { __typename?: 'Query' } & {
     channels: Array<{ __typename?: 'Channel' } & Pick<Channel, 'id' | 'code' | 'token'>>;
 };
 
+export type UpdateChannelMutationVariables = {
+    input: UpdateChannelInput;
+};
+
+export type UpdateChannelMutation = { __typename?: 'Mutation' } & {
+    updateChannel: { __typename?: 'Channel' } & Pick<
+        Channel,
+        'id' | 'code' | 'defaultLanguageCode' | 'currencyCode'
+    >;
+};
+
 export type DeleteChannelMutationVariables = {
     id: Scalars['ID'];
 };
@@ -3593,6 +3604,17 @@ export type DeleteChannelMutation = { __typename?: 'Mutation' } & {
     deleteChannel: { __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'message' | 'result'>;
 };
 
+export type UpdateGlobalSettingsMutationVariables = {
+    input: UpdateGlobalSettingsInput;
+};
+
+export type UpdateGlobalSettingsMutation = { __typename?: 'Mutation' } & {
+    updateGlobalSettings: { __typename?: 'GlobalSettings' } & Pick<
+        GlobalSettings,
+        'id' | 'availableLanguages'
+    >;
+};
+
 export type GetCollectionsWithAssetsQueryVariables = {};
 
 export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
@@ -5454,12 +5476,24 @@ export namespace GetChannels {
     export type Channels = NonNullable<GetChannelsQuery['channels'][0]>;
 }
 
+export namespace UpdateChannel {
+    export type Variables = UpdateChannelMutationVariables;
+    export type Mutation = UpdateChannelMutation;
+    export type UpdateChannel = UpdateChannelMutation['updateChannel'];
+}
+
 export namespace DeleteChannel {
     export type Variables = DeleteChannelMutationVariables;
     export type Mutation = DeleteChannelMutation;
     export type DeleteChannel = DeleteChannelMutation['deleteChannel'];
 }
 
+export namespace UpdateGlobalSettings {
+    export type Variables = UpdateGlobalSettingsMutationVariables;
+    export type Mutation = UpdateGlobalSettingsMutation;
+    export type UpdateGlobalSettings = UpdateGlobalSettingsMutation['updateGlobalSettings'];
+}
+
 export namespace GetCollectionsWithAssets {
     export type Variables = GetCollectionsWithAssetsQueryVariables;
     export type Query = GetCollectionsWithAssetsQuery;

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -39,6 +39,7 @@
     "identifier-change-token-not-recognized": "Identifier change token not recognized",
     "identifier-change-token-has-expired": "Identifier change token has expired",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
+    "language-not-available-in-global-settings": "Language \"{code}\" is not available. First enable it via GlobalSettings and try again.",
     "missing-password-on-registration": "A password must be provided when `authOptions.requireVerification` is set to \"false\"",
     "no-search-plugin-configured": "No search plugin has been configured",
     "no-valid-channel-specified": "No valid channel was specified (ensure the 'vendure-token' header was specified in the request)",

+ 23 - 2
packages/core/src/service/services/channel.service.ts

@@ -13,7 +13,12 @@ import { unique } from '@vendure/common/lib/unique';
 import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
-import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import {
+    ChannelNotFoundError,
+    EntityNotFoundError,
+    InternalServerError,
+    UserInputError,
+} from '../../common/error/errors';
 import { ChannelAware } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -24,11 +29,17 @@ import { Zone } from '../../entity/zone/zone.entity';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
+import { GlobalSettingsService } from './global-settings.service';
+
 @Injectable()
 export class ChannelService {
     private allChannels: Channel[] = [];
 
-    constructor(@InjectConnection() private connection: Connection, private configService: ConfigService) {}
+    constructor(
+        @InjectConnection() private connection: Connection,
+        private configService: ConfigService,
+        private globalSettingsService: GlobalSettingsService,
+    ) {}
 
     /**
      * When the app is bootstrapped, ensure a default Channel exists and populate the
@@ -147,6 +158,16 @@ export class ChannelService {
         if (!channel) {
             throw new EntityNotFoundError('Channel', input.id);
         }
+        if (input.defaultLanguageCode) {
+            const availableLanguageCodes = await this.globalSettingsService
+                .getSettings()
+                .then((s) => s.availableLanguages);
+            if (!availableLanguageCodes.includes(input.defaultLanguageCode)) {
+                throw new UserInputError('error.language-not-available-in-global-settings', {
+                    code: input.defaultLanguageCode,
+                });
+            }
+        }
         const updatedChannel = patchEntity(channel, input);
         if (input.defaultTaxZoneId) {
             updatedChannel.defaultTaxZone = await getEntityOrThrow(