Browse Source

feat(server): Apply ID codec to ConfigurableOperation arguments

Michael Bromley 6 years ago
parent
commit
64c174c316

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

@@ -6,8 +6,8 @@ Object {
     Object {
       "args": Array [
         Object {
-          "name": "percentage",
-          "type": "PERCENTAGE",
+          "name": "facetValueIds",
+          "type": "FACET_VALUE_IDS",
           "value": null,
         },
       ],
@@ -48,9 +48,9 @@ Object {
     Object {
       "args": Array [
         Object {
-          "name": "percentage",
-          "type": "PERCENTAGE",
-          "value": "50",
+          "name": "facetValueIds",
+          "type": "FACET_VALUE_IDS",
+          "value": "[\\"T_1\\"]",
         },
       ],
       "code": "promo_action",
@@ -81,9 +81,9 @@ Object {
     Object {
       "args": Array [
         Object {
-          "name": "percentage",
-          "type": "PERCENTAGE",
-          "value": "50",
+          "name": "facetValueIds",
+          "type": "FACET_VALUE_IDS",
+          "value": "[\\"T_1\\"]",
         },
       ],
       "code": "promo_action",

+ 26 - 4
server/e2e/collection.e2e-spec.ts

@@ -253,14 +253,36 @@ describe('Collection resolver', () => {
         }
     });
 
-    /*describe('filters', () => {
+    describe('filters', () => {
         it('facetValue filter', async () => {
-            const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, { id: electronicsCategory.id });
+            const result = await client.query(GET_COLLECTION_PRODUCT_VARIANTS, {
+                id: electronicsCategory.id,
+            });
             expect(result.collection.productVariants.items.map(i => i.name)).toEqual([
-                '',
+                'Laptop 13 inch 8GB',
+                'Laptop 15 inch 8GB',
+                'Laptop 13 inch 16GB',
+                'Laptop 15 inch 16GB',
+                'Curvy Monitor 24 inch',
+                'Curvy Monitor 27 inch',
+                'Gaming PC i7-8700 240GB SSD',
+                'Gaming PC R7-2700 240GB SSD',
+                'Gaming PC i7-8700 120GB SSD',
+                'Gaming PC R7-2700 120GB SSD',
+                'Hard Drive 1TB',
+                'Hard Drive 2TB',
+                'Hard Drive 3TB',
+                'Hard Drive 4TB',
+                'Hard Drive 6TB',
+                'Clacky Keyboard',
+                'USB Cable',
+                'Instant Camera',
+                'Camera Lens',
+                'Tripod',
+                'SLR Camera',
             ]);
         });
-    });*/
+    });
 });
 
 const GET_COLLECTIONS = gql`

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

@@ -79,7 +79,13 @@ describe('Promotion resolver', () => {
                     actions: [
                         {
                             code: promoAction.code,
-                            arguments: [{ name: 'percentage', value: '50', type: ConfigArgType.PERCENTAGE }],
+                            arguments: [
+                                {
+                                    name: 'facetValueIds',
+                                    value: '["T_1"]',
+                                    type: ConfigArgType.FACET_VALUE_IDS,
+                                },
+                            ],
                         },
                     ],
                 },
@@ -197,7 +203,7 @@ function generateTestAction(code: string): PromotionAction<any> {
     return new PromotionOrderAction({
         code,
         description: `description for ${code}`,
-        args: { percentage: ConfigArgType.PERCENTAGE },
+        args: { facetValueIds: ConfigArgType.FACET_VALUE_IDS },
         execute: (order, args) => {
             return 42;
         },

+ 49 - 0
server/src/api/common/id-codec.service.ts

@@ -1,5 +1,10 @@
 import { Injectable } from '@nestjs/common';
 
+import {
+    ConfigArgType,
+    ConfigurableOperation,
+    ConfigurableOperationInput,
+} from '../../../../shared/generated-types';
 import { ID } from '../../../../shared/shared-types';
 import { ConfigService } from '../../config/config.service';
 
@@ -19,4 +24,48 @@ export class IdCodecService {
     decode<T extends string | number | object | undefined>(target: T, transformKeys?: string[]): T {
         return this.idCodec.decode(target, transformKeys);
     }
+
+    /**
+     * Decodes any entity IDs used in ConfigurableOperation arguments, e.g. when specifying
+     * facetValueIds.
+     */
+    decodeConfigurableOperation(input: ConfigurableOperationInput): ConfigurableOperationInput;
+    decodeConfigurableOperation(input: ConfigurableOperationInput[]): ConfigurableOperationInput[];
+    decodeConfigurableOperation(
+        input: ConfigurableOperationInput | ConfigurableOperationInput[],
+    ): ConfigurableOperationInput | ConfigurableOperationInput[] {
+        const inputArray = Array.isArray(input) ? input : [input];
+        for (const operationInput of inputArray) {
+            for (const arg of operationInput.arguments) {
+                if (arg.type === ConfigArgType.FACET_VALUE_IDS && arg.value) {
+                    const ids = JSON.parse(arg.value) as string[];
+                    const decodedIds = ids.map(id => this.decode(id));
+                    arg.value = JSON.stringify(decodedIds);
+                }
+            }
+        }
+        return Array.isArray(input) ? inputArray : inputArray[0];
+    }
+
+    /**
+     * Encodes any entity IDs used in ConfigurableOperation arguments, e.g. when specifying
+     * facetValueIds.
+     */
+    encodeConfigurableOperation(input: ConfigurableOperation): ConfigurableOperation;
+    encodeConfigurableOperation(input: ConfigurableOperation[]): ConfigurableOperation[];
+    encodeConfigurableOperation(
+        input: ConfigurableOperation | ConfigurableOperation[],
+    ): ConfigurableOperation | ConfigurableOperation[] {
+        const inputArray = Array.isArray(input) ? input : [input];
+        for (const operation of inputArray) {
+            for (const arg of operation.args) {
+                if (arg.type === ConfigArgType.FACET_VALUE_IDS && arg.value) {
+                    const ids = JSON.parse(arg.value) as string[];
+                    const encoded = ids.map(id => this.encode(id));
+                    arg.value = JSON.stringify(encoded);
+                }
+            }
+        }
+        return Array.isArray(input) ? inputArray : inputArray[0];
+    }
 }

+ 20 - 6
server/src/api/resolvers/admin/collection.resolver.ts

@@ -11,7 +11,6 @@ import {
 } from '../../../../../shared/generated-types';
 import { PaginatedList } from '../../../../../shared/shared-types';
 import { Translated } from '../../../common/types/locale-types';
-import { CollectionFilter } from '../../../config/collection/collection-filter';
 import { Collection } from '../../../entity/collection/collection.entity';
 import { CollectionService } from '../../../service/services/collection.service';
 import { FacetValueService } from '../../../service/services/facet-value.service';
@@ -44,7 +43,10 @@ export class CollectionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: CollectionsQueryArgs,
     ): Promise<PaginatedList<Translated<Collection>>> {
-        return this.collectionService.findAll(ctx, args.options || undefined);
+        return this.collectionService.findAll(ctx, args.options || undefined).then(res => {
+            res.items.forEach(this.encodeFilters);
+            return res;
+        });
     }
 
     @Query()
@@ -53,7 +55,7 @@ export class CollectionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: CollectionQueryArgs,
     ): Promise<Translated<Collection> | undefined> {
-        return this.collectionService.findOne(ctx, args.id);
+        return this.collectionService.findOne(ctx, args.id).then(this.encodeFilters);
     }
 
     @Mutation()
@@ -64,7 +66,8 @@ export class CollectionResolver {
         @Args() args: CreateCollectionMutationArgs,
     ): Promise<Translated<Collection>> {
         const { input } = args;
-        return this.collectionService.create(ctx, input);
+        this.idCodecService.decodeConfigurableOperation(input.filters);
+        return this.collectionService.create(ctx, input).then(this.encodeFilters);
     }
 
     @Mutation()
@@ -75,7 +78,8 @@ export class CollectionResolver {
         @Args() args: UpdateCollectionMutationArgs,
     ): Promise<Translated<Collection>> {
         const { input } = args;
-        return this.collectionService.update(ctx, input);
+        this.idCodecService.decodeConfigurableOperation(input.filters);
+        return this.collectionService.update(ctx, input).then(this.encodeFilters);
     }
 
     @Mutation()
@@ -86,6 +90,16 @@ export class CollectionResolver {
         @Args() args: MoveCollectionMutationArgs,
     ): Promise<Translated<Collection>> {
         const { input } = args;
-        return this.collectionService.move(ctx, input);
+        return this.collectionService.move(ctx, input).then(this.encodeFilters);
     }
+
+    /**
+     * Encodes any entity IDs used in the filter arguments.
+     */
+    private encodeFilters = <T extends Collection | undefined>(collection: T): T => {
+        if (collection) {
+            this.idCodecService.encodeConfigurableOperation(collection.filters);
+        }
+        return collection;
+    };
 }

+ 24 - 5
server/src/api/resolvers/admin/promotion.resolver.ts

@@ -12,13 +12,14 @@ import {
 import { PaginatedList } from '../../../../../shared/shared-types';
 import { Promotion } from '../../../entity/promotion/promotion.entity';
 import { PromotionService } from '../../../service/services/promotion.service';
+import { IdCodecService } from '../../common/id-codec.service';
 import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('Promotion')
 export class PromotionResolver {
-    constructor(private promotionService: PromotionService) {}
+    constructor(private promotionService: PromotionService, private idCodecService: IdCodecService) {}
 
     @Query()
     @Allow(Permission.ReadSettings)
@@ -26,13 +27,16 @@ export class PromotionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: PromotionsQueryArgs,
     ): Promise<PaginatedList<Promotion>> {
-        return this.promotionService.findAll(args.options || undefined);
+        return this.promotionService.findAll(args.options || undefined).then(res => {
+            res.items.forEach(this.encodeConditionsAndActions);
+            return res;
+        });
     }
 
     @Query()
     @Allow(Permission.ReadSettings)
     promotion(@Ctx() ctx: RequestContext, @Args() args: PromotionQueryArgs): Promise<Promotion | undefined> {
-        return this.promotionService.findOne(args.id);
+        return this.promotionService.findOne(args.id).then(this.encodeConditionsAndActions);
     }
 
     @Query()
@@ -48,7 +52,9 @@ export class PromotionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: CreatePromotionMutationArgs,
     ): Promise<Promotion> {
-        return this.promotionService.createPromotion(ctx, args.input);
+        this.idCodecService.decodeConfigurableOperation(args.input.actions);
+        this.idCodecService.decodeConfigurableOperation(args.input.conditions);
+        return this.promotionService.createPromotion(ctx, args.input).then(this.encodeConditionsAndActions);
     }
 
     @Mutation()
@@ -57,7 +63,9 @@ export class PromotionResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: UpdatePromotionMutationArgs,
     ): Promise<Promotion> {
-        return this.promotionService.updatePromotion(ctx, args.input);
+        this.idCodecService.decodeConfigurableOperation(args.input.actions || []);
+        this.idCodecService.decodeConfigurableOperation(args.input.conditions || []);
+        return this.promotionService.updatePromotion(ctx, args.input).then(this.encodeConditionsAndActions);
     }
 
     @Mutation()
@@ -65,4 +73,15 @@ export class PromotionResolver {
     deletePromotion(@Args() args: DeletePromotionMutationArgs): Promise<DeletionResponse> {
         return this.promotionService.softDeletePromotion(args.id);
     }
+
+    /**
+     * Encodes any entity IDs used in the filter arguments.
+     */
+    private encodeConditionsAndActions = <T extends Promotion | undefined>(collection: T): T => {
+        if (collection) {
+            this.idCodecService.encodeConfigurableOperation(collection.conditions);
+            this.idCodecService.encodeConfigurableOperation(collection.actions);
+        }
+        return collection;
+    };
 }

+ 12 - 1
server/src/api/resolvers/entity/product-variant-entity.resolver.ts

@@ -1,7 +1,7 @@
 import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
 
 import { Translated } from '../../../common/types/locale-types';
-import { ProductOption } from '../../../entity';
+import { FacetValue, ProductOption } from '../../../entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { RequestContext } from '../../common/request-context';
@@ -21,4 +21,15 @@ export class ProductVariantEntityResolver {
         }
         return this.productVariantService.getOptionsForVariant(ctx, productVariant.id);
     }
+
+    @ResolveProperty()
+    async facetValues(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+    ): Promise<Array<Translated<FacetValue>>> {
+        if (productVariant.facetValues) {
+            return productVariant.facetValues as Array<Translated<FacetValue>>;
+        }
+        return this.productVariantService.getFacetValuesForVariant(ctx, productVariant.id);
+    }
 }

+ 10 - 0
server/src/service/services/product-variant.service.ts

@@ -13,6 +13,7 @@ import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { TaxCategory } from '../../entity';
+import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
@@ -125,6 +126,15 @@ export class ProductVariantService {
             .then(variant => (!variant ? [] : variant.options.map(o => translateDeep(o, ctx.languageCode))));
     }
 
+    getFacetValuesForVariant(ctx: RequestContext, variantId: ID): Promise<Array<Translated<FacetValue>>> {
+        return this.connection
+            .getRepository(ProductVariant)
+            .findOne(variantId, { relations: ['facetValues', 'facetValues.facet'] })
+            .then(variant =>
+                !variant ? [] : variant.facetValues.map(o => translateDeep(o, ctx.languageCode, ['facet'])),
+            );
+    }
+
     async create(
         ctx: RequestContext,
         product: Product,