Browse Source

feat(core): Assign superadmin Role to newly created Channels

Michael Bromley 6 years ago
parent
commit
6fc421a33f

+ 1 - 10
packages/core/e2e/auth.e2e-spec.ts

@@ -7,7 +7,6 @@ import path from 'path';
 
 import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
 import { initialData } from './fixtures/e2e-initial-data';
-import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
     CreateAdministrator,
     CreateRole,
@@ -23,6 +22,7 @@ import {
     CREATE_PRODUCT,
     CREATE_ROLE,
     GET_PRODUCT_LIST,
+    ME,
     UPDATE_PRODUCT,
 } from './graphql/shared-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
@@ -221,12 +221,3 @@ describe('Authorization & permissions', () => {
         };
     }
 });
-
-export const ME = gql`
-    query Me {
-        me {
-            ...CurrentUser
-        }
-    }
-    ${CURRENT_USER_FRAGMENT}
-`;

+ 93 - 0
packages/core/e2e/channel.e2e-spec.ts

@@ -0,0 +1,93 @@
+/* tslint:disable:no-non-null-assertion */
+import { createTestEnvironment } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+import {
+    CreateChannel,
+    CurrencyCode,
+    LanguageCode,
+    Me,
+    Permission,
+} from './graphql/generated-e2e-admin-types';
+import { ME } from './graphql/shared-definitions';
+
+describe('Channels', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
+    const SECOND_CHANNEL_TOKEN = 'second_channel_token';
+
+    beforeAll(async () => {
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('create a new Channel', async () => {
+        const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
+            CREATE_CHANNEL,
+            {
+                input: {
+                    code: 'second-channel',
+                    token: SECOND_CHANNEL_TOKEN,
+                    defaultLanguageCode: LanguageCode.en,
+                    currencyCode: CurrencyCode.GBP,
+                    pricesIncludeTax: true,
+                    defaultShippingZoneId: 'T_1',
+                    defaultTaxZoneId: 'T_1',
+                },
+            },
+        );
+
+        expect(createChannel).toEqual({
+            id: 'T_2',
+            code: 'second-channel',
+            token: SECOND_CHANNEL_TOKEN,
+            currencyCode: 'GBP',
+            defaultLanguageCode: 'en',
+            defaultShippingZone: {
+                id: 'T_1',
+            },
+            defaultTaxZone: {
+                id: 'T_1',
+            },
+            pricesIncludeTax: true,
+        });
+    });
+
+    it('superadmin has all permissions on new channel', async () => {
+        const { me } = await adminClient.query<Me.Query>(ME);
+
+        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);
+    });
+});
+
+const CREATE_CHANNEL = gql`
+    mutation CreateChannel($input: CreateChannelInput!) {
+        createChannel(input: $input) {
+            id
+            code
+            token
+            currencyCode
+            defaultLanguageCode
+            defaultShippingZone {
+                id
+            }
+            defaultTaxZone {
+                id
+            }
+            pricesIncludeTax
+        }
+    }
+`;

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

@@ -2007,6 +2007,11 @@ export type MutationDeleteProductVariantArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationAssignProductToChannelArgs = {
+    productId: Scalars['ID'];
+    channelId: Scalars['ID'];
+};
+
 export type MutationCreatePromotionArgs = {
     input: CreatePromotionInput;
 };
@@ -3407,6 +3412,20 @@ export type MeQuery = { __typename?: 'Query' } & {
     me: Maybe<{ __typename?: 'CurrentUser' } & CurrentUserFragment>;
 };
 
+export type CreateChannelMutationVariables = {
+    input: CreateChannelInput;
+};
+
+export type CreateChannelMutation = { __typename?: 'Mutation' } & {
+    createChannel: { __typename?: 'Channel' } & Pick<
+        Channel,
+        'id' | 'code' | 'token' | 'currencyCode' | 'defaultLanguageCode' | 'pricesIncludeTax'
+    > & {
+            defaultShippingZone: Maybe<{ __typename?: 'Zone' } & Pick<Zone, 'id'>>;
+            defaultTaxZone: Maybe<{ __typename?: 'Zone' } & Pick<Zone, 'id'>>;
+        };
+};
+
 export type GetCollectionsWithAssetsQueryVariables = {};
 
 export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
@@ -5053,6 +5072,16 @@ export namespace Me {
     export type Me = CurrentUserFragment;
 }
 
+export namespace CreateChannel {
+    export type Variables = CreateChannelMutationVariables;
+    export type Mutation = CreateChannelMutation;
+    export type CreateChannel = CreateChannelMutation['createChannel'];
+    export type DefaultShippingZone = NonNullable<
+        CreateChannelMutation['createChannel']['defaultShippingZone']
+    >;
+    export type DefaultTaxZone = NonNullable<CreateChannelMutation['createChannel']['defaultTaxZone']>;
+}
+
 export namespace GetCollectionsWithAssets {
     export type Variables = GetCollectionsWithAssetsQueryVariables;
     export type Query = GetCollectionsWithAssetsQuery;

+ 8 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -283,3 +283,11 @@ export const CREATE_PROMOTION = gql`
     }
     ${PROMOTION_FRAGMENT}
 `;
+export const ME = gql`
+    query Me {
+        me {
+            ...CurrentUser
+        }
+    }
+    ${CURRENT_USER_FRAGMENT}
+`;

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

@@ -8,13 +8,14 @@ import {
 
 import { Channel } from '../../../entity/channel/channel.entity';
 import { ChannelService } from '../../../service/services/channel.service';
+import { RoleService } from '../../../service/services/role.service';
 import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Channel')
 export class ChannelResolver {
-    constructor(private channelService: ChannelService) {}
+    constructor(private channelService: ChannelService, private roleService: RoleService) {}
 
     @Query()
     @Allow(Permission.ReadSettings)
@@ -37,7 +38,10 @@ export class ChannelResolver {
     @Mutation()
     @Allow(Permission.SuperAdmin)
     async createChannel(@Args() args: MutationCreateChannelArgs): Promise<Channel> {
-        return this.channelService.create(args.input);
+        const channel = await this.channelService.create(args.input);
+        const superAdminRole = await this.roleService.getSuperAdminRole();
+        await this.roleService.assignRoleToChannel(superAdminRole.id, channel.id);
+        return channel;
     }
 
     @Mutation()

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

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { CreateChannelInput, CurrencyCode, UpdateChannelInput } from '@vendure/common/lib/generated-types';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
-import { ID } from '@vendure/common/lib/shared-types';
+import { ID, Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { Connection } from 'typeorm';
 
@@ -12,6 +12,7 @@ import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '
 import { ChannelAware } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
+import { VendureEntity } from '../../entity/base/base.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
@@ -36,12 +37,29 @@ export class ChannelService {
      * Assigns a ChannelAware entity to the default Channel as well as any channel
      * specified in the RequestContext.
      */
-    assignToChannels<T extends ChannelAware>(entity: T, ctx: RequestContext): T {
+    assignToCurrentChannel<T extends ChannelAware>(entity: T, ctx: RequestContext): T {
         const channelIds = unique([ctx.channelId, this.getDefaultChannel().id]);
         entity.channels = channelIds.map(id => ({ id })) as any;
         return entity;
     }
 
+    /**
+     * Assigns the entity to the given Channel and saves.
+     */
+    async assignToChannel<T extends ChannelAware & VendureEntity>(
+        entityType: Type<T>,
+        entityId: ID,
+        channelId: ID,
+    ): Promise<T> {
+        const entity = await getEntityOrThrow(this.connection, entityType, entityId, {
+            relations: ['channels'],
+        });
+        const channel = await getEntityOrThrow(this.connection, Channel, channelId);
+        entity.channels.push(channel);
+        await this.connection.getRepository(entityType).save(entity as any);
+        return entity;
+    }
+
     /**
      * Given a channel token, returns the corresponding Channel if it exists.
      */

+ 1 - 1
packages/core/src/service/services/collection.service.ts

@@ -243,7 +243,7 @@ export class CollectionService implements OnModuleInit {
             entityType: Collection,
             translationType: CollectionTranslation,
             beforeSave: async coll => {
-                await this.channelService.assignToChannels(coll, ctx);
+                await this.channelService.assignToCurrentChannel(coll, ctx);
                 const parent = await this.getParentCollection(ctx, input.parentId);
                 if (parent) {
                     coll.parent = parent;

+ 6 - 1
packages/core/src/service/services/product.service.ts

@@ -104,7 +104,7 @@ export class ProductService {
             entityType: Product,
             translationType: ProductTranslation,
             beforeSave: async p => {
-                this.channelService.assignToChannels(p, ctx);
+                this.channelService.assignToCurrentChannel(p, ctx);
                 if (input.facetValueIds) {
                     p.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
                 }
@@ -145,6 +145,11 @@ export class ProductService {
         };
     }
 
+    async assignToChannel(ctx: RequestContext, productId: ID, channelId: ID): Promise<Translated<Product>> {
+        const product = await this.channelService.assignToChannel(Product, productId, channelId);
+        return assertFound(this.findOne(ctx, product.id));
+    }
+
     async addOptionGroupToProduct(
         ctx: RequestContext,
         productId: ID,

+ 1 - 1
packages/core/src/service/services/promotion.service.ts

@@ -104,7 +104,7 @@ export class PromotionService {
             priorityScore: this.calculatePriorityScore(input),
         });
         this.validatePromotionConditions(promotion);
-        this.channelService.assignToChannels(promotion, ctx);
+        this.channelService.assignToCurrentChannel(promotion, ctx);
         const newPromotion = await this.connection.manager.save(promotion);
         await this.updatePromotions();
         return assertFound(this.findOne(newPromotion.id));

+ 4 - 0
packages/core/src/service/services/role.service.ts

@@ -105,6 +105,10 @@ export class RoleService {
         return assertFound(this.findOne(role.id));
     }
 
+    async assignRoleToChannel(roleId: ID, channelId: ID) {
+        await this.channelService.assignToChannel(Role, roleId, channelId);
+    }
+
     private checkPermissionsAreValid(permissions?: string[] | null) {
         if (!permissions) {
             return;