Browse Source

feat(server): Implement delete for Facet & FacetValue entities

Relates to #21
Michael Bromley 7 years ago
parent
commit
84e5474755

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


+ 75 - 0
server/e2e/__snapshots__/facet.e2e-spec.ts.snap

@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Facet resolver createFacet 1`] = `
+Object {
+  "code": "speaker-type",
+  "id": "T_2",
+  "languageCode": "en",
+  "name": "Speaker Type",
+  "translations": Array [
+    Object {
+      "id": "T_3",
+      "languageCode": "en",
+      "name": "Speaker Type",
+    },
+  ],
+  "values": Array [
+    Object {
+      "code": "portable",
+      "facet": Object {
+        "id": "T_2",
+        "name": "Speaker Type",
+      },
+      "id": "T_11",
+      "languageCode": "en",
+      "name": "Portable",
+      "translations": Array [
+        Object {
+          "id": "T_21",
+          "languageCode": "en",
+          "name": "Portable",
+        },
+      ],
+    },
+  ],
+}
+`;
+
+exports[`Facet resolver createFacetValues 1`] = `
+Array [
+  Object {
+    "code": "pc",
+    "facet": Object {
+      "id": "T_2",
+      "name": "Speaker Category",
+    },
+    "id": "T_13",
+    "languageCode": "en",
+    "name": "PC Speakers",
+    "translations": Array [
+      Object {
+        "id": "T_23",
+        "languageCode": "en",
+        "name": "PC Speakers",
+      },
+    ],
+  },
+  Object {
+    "code": "hi-fi",
+    "facet": Object {
+      "id": "T_2",
+      "name": "Speaker Category",
+    },
+    "id": "T_12",
+    "languageCode": "en",
+    "name": "Hi Fi Speakers",
+    "translations": Array [
+      Object {
+        "id": "T_22",
+        "languageCode": "en",
+        "name": "Hi Fi Speakers",
+      },
+    ],
+  },
+]
+`;

+ 1 - 1
server/e2e/__snapshots__/promotion.e2e-spec.ts.snap

@@ -42,7 +42,7 @@ Object {
 }
 `;
 
-exports[`Promotion resolver createPromotion promotion 1`] = `
+exports[`Promotion resolver createPromotion 1`] = `
 Object {
   "actions": Array [
     Object {

+ 339 - 0
server/e2e/facet.e2e-spec.ts

@@ -0,0 +1,339 @@
+import gql from 'graphql-tag';
+
+import {
+    CREATE_FACET,
+    CREATE_FACET_VALUES,
+    GET_FACET_LIST,
+    GET_FACET_WITH_VALUES,
+    UPDATE_FACET,
+    UPDATE_FACET_VALUES,
+} from '../../admin-ui/src/app/data/definitions/facet-definitions';
+import {
+    GET_PRODUCT_LIST,
+    GET_PRODUCT_WITH_VARIANTS,
+    UPDATE_PRODUCT,
+    UPDATE_PRODUCT_VARIANTS,
+} from '../../admin-ui/src/app/data/definitions/product-definitions';
+import {
+    CreateFacet,
+    CreateFacetValues,
+    DeletionResult,
+    FacetWithValues,
+    GetFacetList,
+    GetFacetWithValues,
+    GetProductList,
+    GetProductWithVariants,
+    LanguageCode,
+    UpdateFacet,
+    UpdateFacetValues,
+    UpdateProduct,
+    UpdateProductVariants,
+} 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 brandFacet: FacetWithValues.Fragment;
+    let speakerTypeFacet: FacetWithValues.Fragment;
+
+    beforeAll(async () => {
+        await server.init({
+            productCount: 2,
+            customerCount: 1,
+        });
+        await client.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('createFacet', async () => {
+        const result = await client.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
+            input: {
+                code: 'speaker-type',
+                translations: [{ languageCode: LanguageCode.en, name: 'Speaker Type' }],
+                values: [
+                    {
+                        code: 'portable',
+                        translations: [{ languageCode: LanguageCode.en, name: 'Portable' }],
+                    },
+                ],
+            },
+        });
+
+        speakerTypeFacet = result.createFacet;
+        expect(speakerTypeFacet).toMatchSnapshot();
+    });
+
+    it('updateFacet', async () => {
+        const result = await client.query<UpdateFacet.Mutation, UpdateFacet.Variables>(UPDATE_FACET, {
+            input: {
+                id: speakerTypeFacet.id,
+                translations: [{ languageCode: LanguageCode.en, name: 'Speaker Category' }],
+            },
+        });
+
+        expect(result.updateFacet.name).toBe('Speaker Category');
+    });
+
+    it('createFacetValues', async () => {
+        const result = await client.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
+            CREATE_FACET_VALUES,
+            {
+                input: [
+                    {
+                        facetId: speakerTypeFacet.id,
+                        code: 'pc',
+                        translations: [{ languageCode: LanguageCode.en, name: 'PC Speakers' }],
+                    },
+                    {
+                        facetId: speakerTypeFacet.id,
+                        code: 'hi-fi',
+                        translations: [{ languageCode: LanguageCode.en, name: 'Hi Fi Speakers' }],
+                    },
+                ],
+            },
+        );
+
+        expect(result.createFacetValues).toMatchSnapshot();
+    });
+
+    it('updateFacetValues', async () => {
+        const result = await client.query<UpdateFacetValues.Mutation, UpdateFacetValues.Variables>(
+            UPDATE_FACET_VALUES,
+            {
+                input: [
+                    {
+                        id: speakerTypeFacet.values[0].id,
+                        code: 'compact',
+                    },
+                ],
+            },
+        );
+
+        expect(result.updateFacetValues[0].code).toBe('compact');
+    });
+
+    it('facets', async () => {
+        const result = await client.query<GetFacetList.Query>(GET_FACET_LIST);
+
+        const { items } = result.facets;
+        expect(items.length).toBe(2);
+        expect(items[0].name).toBe('Brand');
+        expect(items[1].name).toBe('Speaker Category');
+
+        brandFacet = items[0];
+        speakerTypeFacet = items[1];
+    });
+
+    it('facet', async () => {
+        const result = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+            GET_FACET_WITH_VALUES,
+            {
+                id: speakerTypeFacet.id,
+            },
+        );
+
+        expect(result.facet!.name).toBe('Speaker Category');
+    });
+
+    describe('deletion', () => {
+        let products: Array<GetProductList.Items & { variants: Array<{ id: string; name: string }> }>;
+
+        beforeAll(async () => {
+            // add the FacetValues to products and variants
+            const result1 = await client.query(GET_PRODUCTS_LIST_WITH_VARIANTS);
+            products = result1.products.items;
+
+            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                input: {
+                    id: products[0].id,
+                    facetValueIds: [speakerTypeFacet.values[0].id],
+                },
+            });
+
+            await client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                UPDATE_PRODUCT_VARIANTS,
+                {
+                    input: [
+                        {
+                            id: products[0].variants[0].id,
+                            facetValueIds: [speakerTypeFacet.values[0].id],
+                        },
+                    ],
+                },
+            );
+
+            await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                input: {
+                    id: products[1].id,
+                    facetValueIds: [speakerTypeFacet.values[1].id],
+                },
+            });
+        });
+
+        it('deleteFacetValues deletes unused facetValue', async () => {
+            const facetValueToDelete = speakerTypeFacet.values[2];
+            const result1 = await client.query(DELETE_FACET_VALUES, {
+                ids: [facetValueToDelete.id],
+                force: false,
+            });
+            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+                GET_FACET_WITH_VALUES,
+                {
+                    id: speakerTypeFacet.id,
+                },
+            );
+
+            expect(result1.deleteFacetValues).toEqual([
+                {
+                    result: DeletionResult.DELETED,
+                    message: ``,
+                },
+            ]);
+
+            expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
+        });
+
+        it('deleteFacetValues for FacetValue in use returns NOT_DELETED', async () => {
+            const facetValueToDelete = speakerTypeFacet.values[0];
+            const result1 = await client.query(DELETE_FACET_VALUES, {
+                ids: [facetValueToDelete.id],
+                force: false,
+            });
+            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+                GET_FACET_WITH_VALUES,
+                {
+                    id: speakerTypeFacet.id,
+                },
+            );
+
+            expect(result1.deleteFacetValues).toEqual([
+                {
+                    result: DeletionResult.NOT_DELETED,
+                    message: `The selected FacetValue is assigned to 1 Product, 1 ProductVariant. To delete anyway, set "force: true"`,
+                },
+            ]);
+
+            expect(result2.facet!.values[0]).toEqual(facetValueToDelete);
+        });
+
+        it('deleteFacetValues for FacetValue in use can be force deleted', async () => {
+            const facetValueToDelete = speakerTypeFacet.values[0];
+            const result1 = await client.query(DELETE_FACET_VALUES, {
+                ids: [facetValueToDelete.id],
+                force: true,
+            });
+
+            expect(result1.deleteFacetValues).toEqual([
+                {
+                    result: DeletionResult.DELETED,
+                    message: `The selected FacetValue was removed from 1 Product, 1 ProductVariant and deleted`,
+                },
+            ]);
+
+            // FacetValue no longer in the Facet.values array
+            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+                GET_FACET_WITH_VALUES,
+                {
+                    id: speakerTypeFacet.id,
+                },
+            );
+            expect(result2.facet!.values[0]).not.toEqual(facetValueToDelete);
+
+            // FacetValue no longer in the Product.facetValues array
+            const result3 = await client.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: products[0].id,
+            });
+            expect(result3.product!.facetValues).toEqual([]);
+        });
+
+        it('deleteFacet that is in use returns NOT_DELETED', async () => {
+            const result1 = await client.query(DELETE_FACET, { id: speakerTypeFacet.id, force: false });
+            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+                GET_FACET_WITH_VALUES,
+                {
+                    id: speakerTypeFacet.id,
+                },
+            );
+
+            expect(result1.deleteFacet).toEqual({
+                result: DeletionResult.NOT_DELETED,
+                message: `The selected Facet includes FacetValues which are assigned to 1 Product. To delete anyway, set "force: true"`,
+            });
+
+            expect(result2.facet).not.toBe(null);
+        });
+
+        it('deleteFacet that is in use can be force deleted', async () => {
+            const result1 = await client.query(DELETE_FACET, { id: speakerTypeFacet.id, force: true });
+
+            expect(result1.deleteFacet).toEqual({
+                result: DeletionResult.DELETED,
+                message: `The Facet was deleted and its FacetValues were removed from 1 Product`,
+            });
+
+            // FacetValue no longer in the Facet.values array
+            const result2 = await client.query<GetFacetWithValues.Query, GetFacetWithValues.Variables>(
+                GET_FACET_WITH_VALUES,
+                {
+                    id: speakerTypeFacet.id,
+                },
+            );
+            expect(result2.facet).toBe(null);
+
+            // FacetValue no longer in the Product.facetValues array
+            const result3 = await client.query<
+                GetProductWithVariants.Query,
+                GetProductWithVariants.Variables
+            >(GET_PRODUCT_WITH_VARIANTS, {
+                id: products[1].id,
+            });
+            expect(result3.product!.facetValues).toEqual([]);
+        });
+    });
+});
+
+const DELETE_FACET_VALUES = gql`
+    mutation DeleteFacetValue($ids: [ID!]!, $force: Boolean) {
+        deleteFacetValues(ids: $ids, force: $force) {
+            result
+            message
+        }
+    }
+`;
+
+const DELETE_FACET = gql`
+    mutation DeleteFacet($id: ID!, $force: Boolean) {
+        deleteFacet(id: $id, force: $force) {
+            result
+            message
+        }
+    }
+`;
+
+const GET_PRODUCTS_LIST_WITH_VARIANTS = gql`
+    query GetProductListWithVariants {
+        products {
+            items {
+                id
+                name
+                variants {
+                    id
+                    name
+                }
+            }
+            totalItems
+        }
+    }
+`;

+ 2 - 2
server/e2e/promotion.e2e-spec.ts

@@ -42,7 +42,7 @@ describe('Promotion resolver', () => {
     let promotion: Promotion.Fragment;
 
     beforeAll(async () => {
-        const token = await server.init(
+        await server.init(
             {
                 productCount: 1,
                 customerCount: 1,
@@ -61,7 +61,7 @@ describe('Promotion resolver', () => {
         await server.destroy();
     });
 
-    it('createPromotion promotion', async () => {
+    it('createPromotion', async () => {
         const result = await client.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
             CREATE_PROMOTION,
             {

+ 24 - 0
server/src/api/resolvers/facet.resolver.ts

@@ -3,6 +3,9 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
     CreateFacetMutationArgs,
     CreateFacetValuesMutationArgs,
+    DeleteFacetMutationArgs,
+    DeleteFacetValuesMutationArgs,
+    DeletionResponse,
     FacetQueryArgs,
     FacetsQueryArgs,
     Permission,
@@ -19,6 +22,7 @@ import { FacetValueService } from '../../service/services/facet-value.service';
 import { FacetService } from '../../service/services/facet.service';
 import { RequestContext } from '../common/request-context';
 import { Allow } from '../decorators/allow.decorator';
+import { Decode } from '../decorators/decode.decorator';
 import { Ctx } from '../decorators/request-context.decorator';
 
 @Resolver('Facet')
@@ -65,8 +69,18 @@ export class FacetResolver {
         return this.facetService.update(args.input);
     }
 
+    @Mutation()
+    @Allow(Permission.DeleteCatalog)
+    async deleteFacet(
+        @Ctx() ctx: RequestContext,
+        @Args() args: DeleteFacetMutationArgs,
+    ): Promise<DeletionResponse> {
+        return this.facetService.delete(ctx, args.id, args.force || false);
+    }
+
     @Mutation()
     @Allow(Permission.CreateCatalog)
+    @Decode('facetId')
     async createFacetValues(
         @Args() args: CreateFacetValuesMutationArgs,
     ): Promise<Array<Translated<FacetValue>>> {
@@ -87,4 +101,14 @@ export class FacetResolver {
         const { input } = args;
         return Promise.all(input.map(facetValue => this.facetValueService.update(facetValue)));
     }
+
+    @Mutation()
+    @Allow(Permission.DeleteCatalog)
+    @Decode('ids')
+    async deleteFacetValues(
+        @Ctx() ctx: RequestContext,
+        @Args() args: DeleteFacetValuesMutationArgs,
+    ): Promise<DeletionResponse[]> {
+        return Promise.all(args.ids.map(id => this.facetValueService.delete(ctx, id, args.force || false)));
+    }
 }

+ 9 - 0
server/src/api/types/facet.api.graphql

@@ -6,12 +6,21 @@ type Query {
 type Mutation {
     "Create a new Facet"
     createFacet(input: CreateFacetInput!): Facet!
+
     "Update an existing Facet"
     updateFacet(input: UpdateFacetInput!): Facet!
+
+    "Delete an existing Facet"
+    deleteFacet(id: ID!, force: Boolean): DeletionResponse!
+
     "Create one or more FacetValues"
     createFacetValues(input: [CreateFacetValueInput!]!): [FacetValue!]!
+
     "Update one or more FacetValues"
     updateFacetValues(input: [UpdateFacetValueInput!]!): [FacetValue!]!
+
+    "Delete one or more FacetValues"
+    deleteFacetValues(ids: [ID!]!, force: Boolean): [DeletionResponse!]!
 }
 
 type FacetList implements PaginatedList {

+ 1 - 1
server/src/entity/facet-value/facet-value-translation.entity.ts

@@ -18,7 +18,7 @@ export class FacetValueTranslation extends VendureEntity implements Translation<
 
     @Column() name: string;
 
-    @ManyToOne(type => FacetValue, base => base.translations)
+    @ManyToOne(type => FacetValue, base => base.translations, { onDelete: 'CASCADE' })
     base: FacetValue;
 
     @Column(type => CustomFacetValueFieldsTranslation)

+ 1 - 1
server/src/entity/facet-value/facet-value.entity.ts

@@ -20,7 +20,7 @@ export class FacetValue extends VendureEntity implements Translatable, HasCustom
     @OneToMany(type => FacetValueTranslation, translation => translation.base, { eager: true })
     translations: Array<Translation<FacetValue>>;
 
-    @ManyToOne(type => Facet, group => group.values)
+    @ManyToOne(type => Facet, group => group.values, { onDelete: 'CASCADE' })
     facet: Facet;
 
     @Column(type => CustomFacetValueFields)

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

@@ -18,7 +18,7 @@ export class FacetTranslation extends VendureEntity implements Translation<Facet
 
     @Column() name: string;
 
-    @ManyToOne(type => Facet, base => base.translations)
+    @ManyToOne(type => Facet, base => base.translations, { onDelete: 'CASCADE' })
     base: Facet;
 
     @Column(type => CustomFacetFieldsTranslation)

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

@@ -21,5 +21,11 @@
     "verification-token-has-expired": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "verification-token-not-recognized": "Verification token not recognized",
     "unauthorized": "The credentials did not match. Please check and try again"
+  },
+  "message": {
+    "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-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-used": "The selected FacetValue is 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\""
   }
 }

+ 57 - 0
server/src/service/services/facet-value.service.ts

@@ -5,6 +5,8 @@ import { Connection } from 'typeorm';
 import {
     CreateFacetValueInput,
     CreateFacetValueWithFacetInput,
+    DeletionResponse,
+    DeletionResult,
     LanguageCode,
     UpdateFacetValueInput,
 } from '../../../../shared/generated-types';
@@ -13,10 +15,12 @@ import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
+import { Product, ProductVariant } from '../../entity';
 import { FacetValueTranslation } from '../../entity/facet-value/facet-value-translation.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { Facet } from '../../entity/facet/facet.entity';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
@@ -94,4 +98,57 @@ export class FacetValueService {
         });
         return assertFound(this.findOne(facetValue.id, DEFAULT_LANGUAGE_CODE));
     }
+
+    async delete(ctx: RequestContext, id: ID, force: boolean = false): Promise<DeletionResponse> {
+        const { productCount, variantCount } = await this.checkFacetValueUsage([id]);
+
+        const isInUse = !!(productCount || variantCount);
+        const both = !!(productCount && variantCount) ? 'both' : 'single';
+        const i18nVars = { products: productCount, variants: variantCount, both };
+        let message = '';
+        let result: DeletionResult;
+
+        if (!isInUse) {
+            const facetValue = await getEntityOrThrow(this.connection, FacetValue, id);
+            await this.connection.getRepository(FacetValue).remove(facetValue);
+            result = DeletionResult.DELETED;
+        } else if (force) {
+            const facetValue = await getEntityOrThrow(this.connection, FacetValue, id);
+            await this.connection.getRepository(FacetValue).remove(facetValue);
+            message = ctx.translate('message.facet-value-force-deleted', i18nVars);
+            result = DeletionResult.DELETED;
+        } else {
+            message = ctx.translate('message.facet-value-used', i18nVars);
+            result = DeletionResult.NOT_DELETED;
+        }
+
+        return {
+            result,
+            message,
+        };
+    }
+
+    /**
+     * Checks for usage of the given FacetValues in any Products or Variants, and returns the counts.
+     */
+    async checkFacetValueUsage(facetValueIds: ID[]): Promise<{ productCount: number; variantCount: number }> {
+        const consumingProducts = await this.connection
+            .getRepository(Product)
+            .createQueryBuilder('product')
+            .leftJoinAndSelect('product.facetValues', 'facetValues')
+            .where('facetValues.id IN (:...facetValueIds)', { facetValueIds })
+            .getMany();
+
+        const consumingVariants = await this.connection
+            .getRepository(ProductVariant)
+            .createQueryBuilder('variant')
+            .leftJoinAndSelect('variant.facetValues', 'facetValues')
+            .where('facetValues.id IN (:...facetValueIds)', { facetValueIds })
+            .getMany();
+
+        return {
+            productCount: consumingProducts.length,
+            variantCount: consumingVariants.length,
+        };
+    }
 }

+ 42 - 1
server/src/service/services/facet.service.ts

@@ -2,8 +2,15 @@ import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { Connection } from 'typeorm';
 
-import { CreateFacetInput, LanguageCode, UpdateFacetInput } from '../../../../shared/generated-types';
+import {
+    CreateFacetInput,
+    DeletionResponse,
+    DeletionResult,
+    LanguageCode,
+    UpdateFacetInput,
+} from '../../../../shared/generated-types';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
+import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
@@ -12,12 +19,16 @@ import { FacetTranslation } from '../../entity/facet/facet-translation.entity';
 import { Facet } from '../../entity/facet/facet.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 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 { FacetValueService } from './facet-value.service';
+
 @Injectable()
 export class FacetService {
     constructor(
         @InjectConnection() private connection: Connection,
+        private facetValueService: FacetValueService,
         private translatableSaver: TranslatableSaver,
         private listQueryBuilder: ListQueryBuilder,
     ) {}
@@ -80,4 +91,34 @@ export class FacetService {
         });
         return assertFound(this.findOne(facet.id, DEFAULT_LANGUAGE_CODE));
     }
+
+    async delete(ctx: RequestContext, id: ID, force: boolean = false): Promise<DeletionResponse> {
+        const facet = await getEntityOrThrow(this.connection, Facet, id, { relations: ['values'] });
+        const { productCount, variantCount } = await this.facetValueService.checkFacetValueUsage(
+            facet.values.map(fv => fv.id),
+        );
+
+        const isInUse = !!(productCount || variantCount);
+        const both = !!(productCount && variantCount) ? 'both' : 'single';
+        const i18nVars = { products: productCount, variants: variantCount, both };
+        let message = '';
+        let result: DeletionResult;
+
+        if (!isInUse) {
+            await this.connection.getRepository(Facet).remove(facet);
+            result = DeletionResult.DELETED;
+        } else if (force) {
+            await this.connection.getRepository(Facet).remove(facet);
+            message = ctx.translate('message.facet-force-deleted', i18nVars);
+            result = DeletionResult.DELETED;
+        } else {
+            message = ctx.translate('message.facet-used', i18nVars);
+            result = DeletionResult.NOT_DELETED;
+        }
+
+        return {
+            result,
+            message,
+        };
+    }
 }

+ 34 - 0
shared/generated-types.ts

@@ -692,8 +692,10 @@ export interface Mutation {
     updateCustomerAddress: Address;
     createFacet: Facet;
     updateFacet: Facet;
+    deleteFacet: DeletionResponse;
     createFacetValues: FacetValue[];
     updateFacetValues: FacetValue[];
+    deleteFacetValues: DeletionResponse[];
     updateGlobalSettings: GlobalSettings;
     importProducts?: ImportInfo | null;
     addItemToOrder?: Order | null;
@@ -1693,12 +1695,20 @@ export interface CreateFacetMutationArgs {
 export interface UpdateFacetMutationArgs {
     input: UpdateFacetInput;
 }
+export interface DeleteFacetMutationArgs {
+    id: string;
+    force?: boolean | null;
+}
 export interface CreateFacetValuesMutationArgs {
     input: CreateFacetValueInput[];
 }
 export interface UpdateFacetValuesMutationArgs {
     input: UpdateFacetValueInput[];
 }
+export interface DeleteFacetValuesMutationArgs {
+    ids: string[];
+    force?: boolean | null;
+}
 export interface UpdateGlobalSettingsMutationArgs {
     input: UpdateGlobalSettingsInput;
 }
@@ -4424,8 +4434,10 @@ export namespace MutationResolvers {
         updateCustomerAddress?: UpdateCustomerAddressResolver<Address, any, Context>;
         createFacet?: CreateFacetResolver<Facet, any, Context>;
         updateFacet?: UpdateFacetResolver<Facet, any, Context>;
+        deleteFacet?: DeleteFacetResolver<DeletionResponse, any, Context>;
         createFacetValues?: CreateFacetValuesResolver<FacetValue[], any, Context>;
         updateFacetValues?: UpdateFacetValuesResolver<FacetValue[], any, Context>;
+        deleteFacetValues?: DeleteFacetValuesResolver<DeletionResponse[], any, Context>;
         updateGlobalSettings?: UpdateGlobalSettingsResolver<GlobalSettings, any, Context>;
         importProducts?: ImportProductsResolver<ImportInfo | null, any, Context>;
         addItemToOrder?: AddItemToOrderResolver<Order | null, any, Context>;
@@ -4711,6 +4723,17 @@ export namespace MutationResolvers {
         input: UpdateFacetInput;
     }
 
+    export type DeleteFacetResolver<R = DeletionResponse, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        DeleteFacetArgs
+    >;
+    export interface DeleteFacetArgs {
+        id: string;
+        force?: boolean | null;
+    }
+
     export type CreateFacetValuesResolver<R = FacetValue[], Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -4731,6 +4754,17 @@ export namespace MutationResolvers {
         input: UpdateFacetValueInput[];
     }
 
+    export type DeleteFacetValuesResolver<R = DeletionResponse[], Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        DeleteFacetValuesArgs
+    >;
+    export interface DeleteFacetValuesArgs {
+        ids: string[];
+        force?: boolean | null;
+    }
+
     export type UpdateGlobalSettingsResolver<R = GlobalSettings, Parent = any, Context = any> = Resolver<
         R,
         Parent,

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