Răsfoiți Sursa

feat(server): Implement delete for Country entity

Relates to #21
Michael Bromley 7 ani în urmă
părinte
comite
21c1813067

+ 126 - 0
server/e2e/country.e2e-spec.ts

@@ -0,0 +1,126 @@
+import gql from 'graphql-tag';
+
+import {
+    CREATE_COUNTRY,
+    GET_AVAILABLE_COUNTRIES,
+    GET_COUNTRY,
+    GET_COUNTRY_LIST,
+    UPDATE_COUNTRY,
+} from '../../admin-ui/src/app/data/definitions/settings-definitions';
+import {
+    CreateCountry,
+    DeletionResult,
+    GetAvailableCountries,
+    GetCountry,
+    GetCountryList,
+    LanguageCode,
+    UpdateCountry,
+} from '../../shared/generated-types';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { TestClient } from './test-client';
+import { TestServer } from './test-server';
+
+// tslint:disable:no-non-null-assertion
+
+describe('Facet resolver', () => {
+    const client = new TestClient();
+    const server = new TestServer();
+    let countries: GetCountryList.Items[];
+    let GB: GetCountryList.Items;
+    let AT: GetCountryList.Items;
+
+    beforeAll(async () => {
+        await server.init({
+            productCount: 2,
+            customerCount: 1,
+        });
+        await client.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('countries', async () => {
+        const result = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+
+        expect(result.countries.totalItems).toBe(9);
+        countries = result.countries.items;
+        GB = countries.find(c => c.code === 'GB')!;
+        AT = countries.find(c => c.code === 'AT')!;
+    });
+
+    it('country', async () => {
+        const result = await client.query<GetCountry.Query, GetCountry.Variables>(GET_COUNTRY, {
+            id: GB.id,
+        });
+
+        expect(result.country!.name).toBe(GB.name);
+    });
+
+    it('updateCountry', async () => {
+        const result = await client.query<UpdateCountry.Mutation, UpdateCountry.Variables>(UPDATE_COUNTRY, {
+            input: {
+                id: AT.id,
+                enabled: false,
+            },
+        });
+
+        expect(result.updateCountry.enabled).toBe(false);
+    });
+
+    it('availableCountries returns enabled countries', async () => {
+        const result = await client.query<GetAvailableCountries.Query>(GET_AVAILABLE_COUNTRIES);
+
+        expect(result.availableCountries.length).toBe(countries.length - 1);
+        expect(result.availableCountries.find(c => c.id === AT.id)).toBeUndefined();
+    });
+
+    it('createCountry', async () => {
+        const result = await client.query<CreateCountry.Mutation, CreateCountry.Variables>(CREATE_COUNTRY, {
+            input: {
+                code: 'GL',
+                enabled: true,
+                translations: [{ languageCode: LanguageCode.en, name: 'Gondwanaland' }],
+            },
+        });
+
+        expect(result.createCountry.name).toBe('Gondwanaland');
+    });
+
+    describe('deletion', () => {
+        it('deletes Country not used in any address', async () => {
+            const result1 = await client.query(DELETE_COUNTRY, { id: AT.id });
+
+            expect(result1.deleteCountry).toEqual({
+                result: DeletionResult.DELETED,
+                message: '',
+            });
+
+            const result2 = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+            expect(result2.countries.items.find(c => c.id === AT.id)).toBeUndefined();
+        });
+
+        it('does not delete Country that is used in one or more addresses', async () => {
+            const result1 = await client.query(DELETE_COUNTRY, { id: GB.id });
+
+            expect(result1.deleteCountry).toEqual({
+                result: DeletionResult.NOT_DELETED,
+                message: 'The selected Country cannot be deleted as it is used in 1 Address',
+            });
+
+            const result2 = await client.query<GetCountryList.Query>(GET_COUNTRY_LIST, {});
+            expect(result2.countries.items.find(c => c.id === GB.id)).not.toBeUndefined();
+        });
+    });
+});
+
+const DELETE_COUNTRY = gql`
+    mutation DeleteCountry($id: ID!) {
+        deleteCountry(id: $id) {
+            result
+            message
+        }
+    }
+`;

+ 11 - 0
server/src/api/resolvers/country.resolver.ts

@@ -4,6 +4,8 @@ import {
     CountriesQueryArgs,
     CountriesQueryArgs,
     CountryQueryArgs,
     CountryQueryArgs,
     CreateCountryMutationArgs,
     CreateCountryMutationArgs,
+    DeleteCountryMutationArgs,
+    DeletionResponse,
     Permission,
     Permission,
     UpdateCountryMutationArgs,
     UpdateCountryMutationArgs,
 } from '../../../../shared/generated-types';
 } from '../../../../shared/generated-types';
@@ -73,4 +75,13 @@ export class CountryResolver {
     ): Promise<Translated<Country>> {
     ): Promise<Translated<Country>> {
         return this.countryService.update(ctx, args.input);
         return this.countryService.update(ctx, args.input);
     }
     }
+
+    @Mutation()
+    @Allow(Permission.DeleteSettings)
+    async deleteCountry(
+        @Ctx() ctx: RequestContext,
+        @Args() args: DeleteCountryMutationArgs,
+    ): Promise<DeletionResponse> {
+        return this.countryService.delete(ctx, args.id);
+    }
 }
 }

+ 4 - 0
server/src/api/types/country.api.graphql

@@ -7,8 +7,12 @@ type Query {
 type Mutation {
 type Mutation {
     "Create a new Country"
     "Create a new Country"
     createCountry(input: CreateCountryInput!): Country!
     createCountry(input: CreateCountryInput!): Country!
+
     "Update an existing Country"
     "Update an existing Country"
     updateCountry(input: UpdateCountryInput!): Country!
     updateCountry(input: UpdateCountryInput!): Country!
+
+    "Delete a Country"
+    deleteCountry(id: ID!): DeletionResponse!
 }
 }
 
 
 type CountryList implements PaginatedList {
 type CountryList implements PaginatedList {

+ 1 - 1
server/src/entity/country/country-translation.entity.ts

@@ -17,6 +17,6 @@ export class CountryTranslation extends VendureEntity implements Translation<Cou
 
 
     @Column() name: string;
     @Column() name: string;
 
 
-    @ManyToOne(type => Country, base => base.translations)
+    @ManyToOne(type => Country, base => base.translations, { onDelete: 'CASCADE' })
     base: Country;
     base: Country;
 }
 }

+ 1 - 0
server/src/i18n/messages/en.json

@@ -23,6 +23,7 @@
     "unauthorized": "The credentials did not match. Please check and try again"
     "unauthorized": "The credentials did not match. Please check and try again"
   },
   },
   "message": {
   "message": {
+    "country-used-in-addresses": "The selected Country cannot be deleted as it is used in {count, plural, one {1 Address} other {# Addresses}}",
     "facet-force-deleted": "The Facet was deleted and its FacetValues were removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-force-deleted": "The Facet was deleted and its FacetValues were removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-used": "The selected Facet includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}. To delete anyway, set \"force: true\"",
     "facet-used": "The selected Facet includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}. To delete anyway, set \"force: true\"",
     "facet-value-force-deleted": "The selected FacetValue was removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}} and deleted",
     "facet-value-force-deleted": "The selected FacetValue was removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}} and deleted",

+ 30 - 1
server/src/service/services/country.service.ts

@@ -2,17 +2,24 @@ import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { InjectConnection } from '@nestjs/typeorm';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
-import { CreateCountryInput, UpdateCountryInput } from '../../../../shared/generated-types';
+import {
+    CreateCountryInput,
+    DeletionResponse,
+    DeletionResult,
+    UpdateCountryInput,
+} from '../../../../shared/generated-types';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
+import { Address } from '../../entity';
 import { CountryTranslation } from '../../entity/country/country-translation.entity';
 import { CountryTranslation } from '../../entity/country/country-translation.entity';
 import { Country } from '../../entity/country/country.entity';
 import { Country } from '../../entity/country/country.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 
 @Injectable()
 @Injectable()
@@ -75,4 +82,26 @@ export class CountryService {
         });
         });
         return assertFound(this.findOne(ctx, country.id));
         return assertFound(this.findOne(ctx, country.id));
     }
     }
+
+    async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
+        const country = await getEntityOrThrow(this.connection, Country, id);
+        const addressesUsingCountry = await this.connection
+            .getRepository(Address)
+            .createQueryBuilder('address')
+            .where('address.countryId = :id', { id })
+            .getCount();
+
+        if (0 < addressesUsingCountry) {
+            return {
+                result: DeletionResult.NOT_DELETED,
+                message: ctx.translate('message.country-used-in-addresses', { count: addressesUsingCountry }),
+            };
+        } else {
+            await this.connection.getRepository(Country).remove(country);
+            return {
+                result: DeletionResult.DELETED,
+                message: '',
+            };
+        }
+    }
 }
 }