Browse Source

feat(server): Implement soft delete for Promotion entity

Relates to #21
Michael Bromley 7 years ago
parent
commit
369b6586b4

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


+ 62 - 8
server/e2e/promotion.e2e-spec.ts

@@ -1,3 +1,12 @@
+import gql from 'graphql-tag';
+
+import {
+    CREATE_PROMOTION,
+    GET_ADJUSTMENT_OPERATIONS,
+    GET_PROMOTION,
+    GET_PROMOTION_LIST,
+    UPDATE_PROMOTION,
+} from '../../admin-ui/src/app/data/definitions/promotion-definitions';
 import {
     CreatePromotion,
     GetAdjustmentOperations,
@@ -7,20 +16,13 @@ import {
     UpdatePromotion,
 } from '../../shared/generated-types';
 import { pick } from '../../shared/pick';
-
-import {
-    CREATE_PROMOTION,
-    GET_ADJUSTMENT_OPERATIONS,
-    GET_PROMOTION,
-    GET_PROMOTION_LIST,
-    UPDATE_PROMOTION,
-} from '../../admin-ui/src/app/data/definitions/promotion-definitions';
 import { PromotionAction, PromotionOrderAction } from '../src/config/promotion/promotion-action';
 import { PromotionCondition } from '../src/config/promotion/promotion-condition';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { TestClient } from './test-client';
 import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './test-utils';
 
 // tslint:disable:no-non-null-assertion
 
@@ -131,6 +133,52 @@ describe('Promotion resolver', () => {
 
         expect(result.adjustmentOperations).toMatchSnapshot();
     });
+
+    describe('deletion', () => {
+        let allPromotions: GetPromotionList.Items[];
+        let promotionToDelete: GetPromotionList.Items;
+
+        beforeAll(async () => {
+            const result = await client.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
+            allPromotions = result.promotions.items;
+        });
+
+        it('deletes a promotion', async () => {
+            promotionToDelete = allPromotions[0];
+            const result = await client.query(DELETE_PROMOTION, { id: promotionToDelete.id });
+
+            expect(result.deletePromotion).toBe(true);
+        });
+
+        it('cannot get a deleted promotion', async () => {
+            const result = await client.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
+                id: promotionToDelete.id,
+            });
+
+            expect(result.promotion).toBe(null);
+        });
+
+        it('deleted promotion omitted from list', async () => {
+            const result = await client.query<GetPromotionList.Query>(GET_PROMOTION_LIST);
+
+            expect(result.promotions.items.length).toBe(allPromotions.length - 1);
+            expect(result.promotions.items.map(c => c.id).includes(promotionToDelete.id)).toBe(false);
+        });
+
+        it(
+            'updatePromotion throws for deleted promotion',
+            assertThrowsWithMessage(
+                () =>
+                    client.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
+                        input: {
+                            id: promotionToDelete.id,
+                            enabled: false,
+                        },
+                    }),
+                `No Promotion with the id '1' could be found`,
+            ),
+        );
+    });
 });
 
 function generateTestCondition(code: string): PromotionCondition<any> {
@@ -152,3 +200,9 @@ function generateTestAction(code: string): PromotionAction<any> {
         },
     });
 }
+
+const DELETE_PROMOTION = gql`
+    mutation DeletePromotion($id: ID!) {
+        deletePromotion(id: $id)
+    }
+`;

+ 7 - 0
server/src/api/resolvers/promotion.resolver.ts

@@ -2,6 +2,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 
 import {
     CreatePromotionMutationArgs,
+    DeletePromotionMutationArgs,
     Permission,
     PromotionQueryArgs,
     PromotionsQueryArgs,
@@ -57,4 +58,10 @@ export class PromotionResolver {
     ): Promise<Promotion> {
         return this.promotionService.updatePromotion(ctx, args.input);
     }
+
+    @Mutation()
+    @Allow(Permission.DeleteSettings)
+    deletePromotion(@Args() args: DeletePromotionMutationArgs): Promise<boolean> {
+        return this.promotionService.softDeletePromotion(args.id);
+    }
 }

+ 1 - 0
server/src/api/types/promotion.api.graphql

@@ -12,6 +12,7 @@ type AdjustmentOperations {
 type Mutation {
     createPromotion(input: CreatePromotionInput!): Promotion!
     updatePromotion(input: UpdatePromotionInput!): Promotion!
+    deletePromotion(id: ID!): Boolean
 }
 
 type PromotionList implements PaginatedList {

+ 5 - 2
server/src/entity/promotion/promotion.entity.ts

@@ -3,7 +3,7 @@ import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
 import { Adjustment, AdjustmentOperation, AdjustmentType } from '../../../../shared/generated-types';
 import { DeepPartial } from '../../../../shared/shared-types';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
-import { ChannelAware } from '../../common/types/common-types';
+import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
 import { getConfig } from '../../config/config-helpers';
 import {
     PromotionAction,
@@ -29,7 +29,7 @@ export interface ApplyOrderActionArgs {
 }
 
 @Entity()
-export class Promotion extends AdjustmentSource implements ChannelAware {
+export class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable {
     type = AdjustmentType.PROMOTION;
     private readonly allConditions: { [code: string]: PromotionCondition } = {};
     private readonly allActions: { [code: string]: PromotionItemAction | PromotionOrderAction } = {};
@@ -49,6 +49,9 @@ export class Promotion extends AdjustmentSource implements ChannelAware {
         this.allActions = actions.reduce((hash, o) => ({ ...hash, [o.code]: o }), {});
     }
 
+    @Column({ type: Date, nullable: true, default: null })
+    deletedAt: Date | null;
+
     @Column() name: string;
 
     @Column() enabled: boolean;

+ 11 - 7
server/src/service/services/promotion.service.ts

@@ -11,7 +11,7 @@ import {
 import { omit } from '../../../../shared/omit';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
 import { RequestContext } from '../../api/common/request-context';
-import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
+import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -19,6 +19,7 @@ import { PromotionAction } from '../../config/promotion/promotion-action';
 import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 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';
 
 import { ChannelService } from './channel.service';
@@ -46,7 +47,7 @@ export class PromotionService {
 
     findAll(options?: ListQueryOptions<Promotion>): Promise<PaginatedList<Promotion>> {
         return this.listQueryBuilder
-            .build(Promotion, options)
+            .build(Promotion, options, { where: { deletedAt: null } })
             .getManyAndCount()
             .then(([items, totalItems]) => ({
                 items,
@@ -55,7 +56,7 @@ export class PromotionService {
     }
 
     async findOne(adjustmentSourceId: ID): Promise<Promotion | undefined> {
-        return this.connection.manager.findOne(Promotion, adjustmentSourceId, {});
+        return this.connection.manager.findOne(Promotion, adjustmentSourceId, { where: { deletedAt: null } });
     }
 
     /**
@@ -103,10 +104,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 EntityNotFoundError('Promotion', input.id);
-        }
+        const adjustmentSource = await getEntityOrThrow(this.connection, Promotion, input.id);
         const updatedAdjustmentSource = patchEntity(adjustmentSource, omit(input, ['conditions', 'actions']));
         if (input.conditions) {
             updatedAdjustmentSource.conditions = input.conditions.map(c =>
@@ -122,6 +120,12 @@ export class PromotionService {
         return assertFound(this.findOne(updatedAdjustmentSource.id));
     }
 
+    async softDeletePromotion(promotionId: ID): Promise<boolean> {
+        await getEntityOrThrow(this.connection, Promotion, promotionId);
+        await this.connection.getRepository(Promotion).update({ id: promotionId }, { deletedAt: new Date() });
+        return true;
+    }
+
     /**
      * Converts the input values of the "create" and "update" mutations into the format expected by the AdjustmentSource entity.
      */

+ 15 - 0
shared/generated-types.ts

@@ -719,6 +719,7 @@ export interface Mutation {
     updateProductVariants: (ProductVariant | null)[];
     createPromotion: Promotion;
     updatePromotion: Promotion;
+    deletePromotion?: boolean | null;
     createRole: Role;
     updateRole: Role;
     reindex: boolean;
@@ -1775,6 +1776,9 @@ export interface CreatePromotionMutationArgs {
 export interface UpdatePromotionMutationArgs {
     input: UpdatePromotionInput;
 }
+export interface DeletePromotionMutationArgs {
+    id: string;
+}
 export interface CreateRoleMutationArgs {
     input: CreateRoleInput;
 }
@@ -4437,6 +4441,7 @@ export namespace MutationResolvers {
         updateProductVariants?: UpdateProductVariantsResolver<(ProductVariant | null)[], any, Context>;
         createPromotion?: CreatePromotionResolver<Promotion, any, Context>;
         updatePromotion?: UpdatePromotionResolver<Promotion, any, Context>;
+        deletePromotion?: DeletePromotionResolver<boolean | null, any, Context>;
         createRole?: CreateRoleResolver<Role, any, Context>;
         updateRole?: UpdateRoleResolver<Role, any, Context>;
         reindex?: ReindexResolver<boolean, any, Context>;
@@ -4970,6 +4975,16 @@ export namespace MutationResolvers {
         input: UpdatePromotionInput;
     }
 
+    export type DeletePromotionResolver<R = boolean | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        DeletePromotionArgs
+    >;
+    export interface DeletePromotionArgs {
+        id: string;
+    }
+
     export type CreateRoleResolver<R = Role, Parent = any, Context = any> = Resolver<
         R,
         Parent,

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