Browse Source

feat(server): Implement soft delete for Customer entity

Relates to #21
Michael Bromley 7 years ago
parent
commit
9b91b0bef6

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


+ 82 - 5
server/e2e/customer.e2e-spec.ts

@@ -1,10 +1,19 @@
 import gql from 'graphql-tag';
 
 import {
+    CREATE_CUSTOMER_ADDRESS,
     GET_CUSTOMER,
     GET_CUSTOMER_LIST,
+    UPDATE_CUSTOMER,
+    UPDATE_CUSTOMER_ADDRESS,
 } from '../../admin-ui/src/app/data/definitions/customer-definitions';
-import { GetCustomer, GetCustomerList } from '../../shared/generated-types';
+import {
+    CreateCustomerAddress,
+    GetCustomer,
+    GetCustomerList,
+    UpdateCustomer,
+    UpdateCustomerAddress,
+} from '../../shared/generated-types';
 import { omit } from '../../shared/omit';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
@@ -17,7 +26,8 @@ describe('Customer resolver', () => {
     const client = new TestClient();
     const server = new TestServer();
     let firstCustomer: GetCustomerList.Items;
-    let secondsCustomer: GetCustomerList.Items;
+    let secondCustomer: GetCustomerList.Items;
+    let thirdCustomer: GetCustomerList.Items;
 
     beforeAll(async () => {
         const token = await server.init({
@@ -39,7 +49,8 @@ describe('Customer resolver', () => {
         expect(result.customers.items.length).toBe(5);
         expect(result.customers.totalItems).toBe(5);
         firstCustomer = result.customers.items[0];
-        secondsCustomer = result.customers.items[1];
+        secondCustomer = result.customers.items[1];
+        thirdCustomer = result.customers.items[2];
     });
 
     describe('addresses', () => {
@@ -143,7 +154,7 @@ describe('Customer resolver', () => {
 
             // get the second customer's address id
             const result5 = await client.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
-                id: secondsCustomer.id,
+                id: secondCustomer.id,
             });
             const secondCustomerAddressId = result5.customer!.addresses![0].id;
 
@@ -206,7 +217,7 @@ describe('Customer resolver', () => {
     });
 
     describe('orders', () => {
-        it("lists that user's orders", async () => {
+        it(`lists that user\'s orders`, async () => {
             // log in as first customer
             await client.asUserWithCredentials(firstCustomer.emailAddress, 'test');
             // add an item to the order to create an order
@@ -223,6 +234,66 @@ describe('Customer resolver', () => {
             expect(result2.customer.orders.items[0].id).toBe(result1.addItemToOrder.id);
         });
     });
+
+    describe('deletion', () => {
+        it('deletes a customer', async () => {
+            const result = await client.query(DELETE_CUSTOMER, { id: thirdCustomer.id });
+
+            expect(result.deleteCustomer).toBe(true);
+        });
+
+        it('cannot get a deleted customer', async () => {
+            const result = await client.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
+                id: thirdCustomer.id,
+            });
+
+            expect(result.customer).toBe(null);
+        });
+
+        it('deleted customer omitted from list', async () => {
+            const result = await client.query<GetCustomerList.Query, GetCustomerList.Variables>(
+                GET_CUSTOMER_LIST,
+            );
+
+            expect(result.customers.items.map(c => c.id).includes(thirdCustomer.id)).toBe(false);
+        });
+
+        it('updateCustomer throws for deleted customer', async () => {
+            try {
+                await client.query<UpdateCustomer.Mutation, UpdateCustomer.Variables>(UPDATE_CUSTOMER, {
+                    input: {
+                        id: thirdCustomer.id,
+                        firstName: 'updated',
+                    },
+                });
+                fail('Should have thrown');
+            } catch (err) {
+                expect(err.message).toEqual(
+                    expect.stringContaining(`No Customer with the id '3' could be found`),
+                );
+            }
+        });
+
+        it('createCustomerAddress throws for deleted customer', async () => {
+            try {
+                await client.query<CreateCustomerAddress.Mutation, CreateCustomerAddress.Variables>(
+                    CREATE_CUSTOMER_ADDRESS,
+                    {
+                        customerId: thirdCustomer.id,
+                        input: {
+                            streetLine1: 'test',
+                            countryCode: 'GB',
+                        },
+                    },
+                );
+                fail('Should have thrown');
+            } catch (err) {
+                expect(err.message).toEqual(
+                    expect.stringContaining(`No Customer with the id '3' could be found`),
+                );
+            }
+        });
+    });
 });
 
 const CREATE_ADDRESS = gql`
@@ -275,3 +346,9 @@ const GET_CUSTOMER_ORDERS = gql`
         }
     }
 `;
+
+const DELETE_CUSTOMER = gql`
+    mutation DeleteCustomer($id: ID!) {
+        deleteCustomer(id: $id)
+    }
+`;

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

@@ -5,6 +5,7 @@ import {
     CreateCustomerMutationArgs,
     CustomerQueryArgs,
     CustomersQueryArgs,
+    DeleteCustomerMutationArgs,
     OrdersCustomerArgs,
     Permission,
     UpdateCustomerAddressMutationArgs,
@@ -105,6 +106,12 @@ export class CustomerResolver {
         return this.customerService.updateAddress(input);
     }
 
+    @Mutation()
+    @Allow(Permission.DeleteCustomer)
+    async deleteCustomer(@Args() args: DeleteCustomerMutationArgs): Promise<boolean> {
+        return this.customerService.softDelete(args.id);
+    }
+
     /**
      * If the current request is authorized as the Owner, ensure that the userId matches that
      * of the Customer data being requested.

+ 6 - 0
server/src/api/types/customer.api.graphql

@@ -7,10 +7,16 @@ type Query {
 type Mutation {
     "Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer."
     createCustomer(input: CreateCustomerInput!, password: String): Customer!
+
     "Update an existing Customer"
     updateCustomer(input: UpdateCustomerInput!): Customer!
+
+    "Delete a Customer"
+    deleteCustomer(id: ID!): Boolean!
+
     "Create a new Address and associate it with the Customer specified by customerId"
     createCustomerAddress(customerId: ID!, input: CreateAddressInput!): Address!
+
     "Update an existing Address"
     updateCustomerAddress(input: UpdateAddressInput!): Address!
 }

+ 7 - 0
server/src/common/types/common-types.ts

@@ -10,6 +10,13 @@ export interface ChannelAware {
     channels: Channel[];
 }
 
+/**
+ * Entities which can be soft deleted should implement this interface.
+ */
+export interface SoftDeletable {
+    deletedAt: Date;
+}
+
 /**
  * Creates a type based on T, but with all properties non-optional
  * and readonly.

+ 5 - 1
server/src/entity/customer/customer.entity.ts

@@ -2,6 +2,7 @@ import { Column, Entity, JoinColumn, JoinTable, ManyToMany, OneToMany, OneToOne
 
 import { DeepPartial } from '../../../../shared/shared-types';
 import { HasCustomFields } from '../../../../shared/shared-types';
+import { SoftDeletable } from '../../common/types/common-types';
 import { Address } from '../address/address.entity';
 import { VendureEntity } from '../base/base.entity';
 import { CustomCustomerFields } from '../custom-entity-fields';
@@ -10,11 +11,14 @@ import { Order } from '../order/order.entity';
 import { User } from '../user/user.entity';
 
 @Entity()
-export class Customer extends VendureEntity implements HasCustomFields {
+export class Customer extends VendureEntity implements HasCustomFields, SoftDeletable {
     constructor(input?: DeepPartial<Customer>) {
         super(input);
     }
 
+    @Column({ nullable: true, default: null })
+    deletedAt: Date;
+
     @Column({ nullable: true })
     title: string;
 

+ 1 - 1
server/src/service/services/customer-group.service.ts

@@ -63,6 +63,6 @@ export class CustomerGroupService {
     }
 
     private getCustomersFromIds(ids: ID[]): Promise<Customer[]> {
-        return this.connection.getRepository(Customer).findByIds(ids);
+        return this.connection.getRepository(Customer).findByIds(ids, { where: { deletedAt: null } });
     }
 }

+ 13 - 1
server/src/service/services/customer.service.ts

@@ -38,18 +38,20 @@ export class CustomerService {
     findAll(options: ListQueryOptions<Customer> | undefined): Promise<PaginatedList<Customer>> {
         return this.listQueryBuilder
             .build(Customer, options)
+            .andWhere('customer.deletedAt IS NULL')
             .getManyAndCount()
             .then(([items, totalItems]) => ({ items, totalItems }));
     }
 
     findOne(id: ID): Promise<Customer | undefined> {
-        return this.connection.getRepository(Customer).findOne(id);
+        return this.connection.getRepository(Customer).findOne(id, { where: { deletedAt: null } });
     }
 
     findOneByUserId(userId: ID): Promise<Customer | undefined> {
         return this.connection.getRepository(Customer).findOne({
             where: {
                 user: { id: userId },
+                deletedAt: null,
             },
         });
     }
@@ -131,6 +133,9 @@ export class CustomerService {
 
     async update(input: UpdateCustomerInput): Promise<Customer> {
         const customer = await getEntityOrThrow(this.connection, Customer, input.id);
+        if (customer.deletedAt) {
+            throw new EntityNotFoundError('Customer', input.id);
+        }
         const updatedCustomer = patchEntity(customer, input);
         await this.connection.getRepository(Customer).save(customer);
         return assertFound(this.findOne(customer.id));
@@ -161,6 +166,7 @@ export class CustomerService {
         input: CreateAddressInput,
     ): Promise<Address> {
         const customer = await this.connection.manager.findOne(Customer, customerId, {
+            where: { deletedAt: null },
             relations: ['addresses'],
         });
 
@@ -187,6 +193,12 @@ export class CustomerService {
         return updatedAddress;
     }
 
+    async softDelete(customerId: ID): Promise<boolean> {
+        await getEntityOrThrow(this.connection, Customer, customerId);
+        await this.connection.getRepository(Customer).update({ id: customerId }, { deletedAt: new Date() });
+        return true;
+    }
+
     private async enforceSingleDefaultAddress(addressId: ID, input: CreateAddressInput | UpdateAddressInput) {
         const result = await this.connection
             .getRepository(Address)

+ 30 - 0
shared/generated-types.ts

@@ -687,6 +687,7 @@ export interface Mutation {
     removeCustomersFromGroup: CustomerGroup;
     createCustomer: Customer;
     updateCustomer: Customer;
+    deleteCustomer: boolean;
     createCustomerAddress: Address;
     updateCustomerAddress: Address;
     createFacet: Facet;
@@ -711,6 +712,7 @@ export interface Mutation {
     updateProductOptionGroup: ProductOptionGroup;
     createProduct: Product;
     updateProduct: Product;
+    deleteProduct?: boolean | null;
     addOptionGroupToProduct: Product;
     removeOptionGroupFromProduct: Product;
     generateVariantsForProduct: Product;
@@ -1669,6 +1671,9 @@ export interface CreateCustomerMutationArgs {
 export interface UpdateCustomerMutationArgs {
     input: UpdateCustomerInput;
 }
+export interface DeleteCustomerMutationArgs {
+    id: string;
+}
 export interface CreateCustomerAddressMutationArgs {
     customerId: string;
     input: CreateAddressInput;
@@ -1744,6 +1749,9 @@ export interface CreateProductMutationArgs {
 export interface UpdateProductMutationArgs {
     input: UpdateProductInput;
 }
+export interface DeleteProductMutationArgs {
+    id: string;
+}
 export interface AddOptionGroupToProductMutationArgs {
     productId: string;
     optionGroupId: string;
@@ -4397,6 +4405,7 @@ export namespace MutationResolvers {
         removeCustomersFromGroup?: RemoveCustomersFromGroupResolver<CustomerGroup, any, Context>;
         createCustomer?: CreateCustomerResolver<Customer, any, Context>;
         updateCustomer?: UpdateCustomerResolver<Customer, any, Context>;
+        deleteCustomer?: DeleteCustomerResolver<boolean, any, Context>;
         createCustomerAddress?: CreateCustomerAddressResolver<Address, any, Context>;
         updateCustomerAddress?: UpdateCustomerAddressResolver<Address, any, Context>;
         createFacet?: CreateFacetResolver<Facet, any, Context>;
@@ -4421,6 +4430,7 @@ export namespace MutationResolvers {
         updateProductOptionGroup?: UpdateProductOptionGroupResolver<ProductOptionGroup, any, Context>;
         createProduct?: CreateProductResolver<Product, any, Context>;
         updateProduct?: UpdateProductResolver<Product, any, Context>;
+        deleteProduct?: DeleteProductResolver<boolean | null, any, Context>;
         addOptionGroupToProduct?: AddOptionGroupToProductResolver<Product, any, Context>;
         removeOptionGroupFromProduct?: RemoveOptionGroupFromProductResolver<Product, any, Context>;
         generateVariantsForProduct?: GenerateVariantsForProductResolver<Product, any, Context>;
@@ -4635,6 +4645,16 @@ export namespace MutationResolvers {
         input: UpdateCustomerInput;
     }
 
+    export type DeleteCustomerResolver<R = boolean, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        DeleteCustomerArgs
+    >;
+    export interface DeleteCustomerArgs {
+        id: string;
+    }
+
     export type CreateCustomerAddressResolver<R = Address, Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -4876,6 +4896,16 @@ export namespace MutationResolvers {
         input: UpdateProductInput;
     }
 
+    export type DeleteProductResolver<R = boolean | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        DeleteProductArgs
+    >;
+    export interface DeleteProductArgs {
+        id: string;
+    }
+
     export type AddOptionGroupToProductResolver<R = Product, Parent = any, Context = any> = Resolver<
         R,
         Parent,

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