Browse Source

refactor(elasticsearch-plugin): Rename option & improve type safety

Michael Bromley 4 years ago
parent
commit
52f0dbe89c

+ 62 - 18
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -8,6 +8,7 @@ import {
     Channel,
     Channel,
     Collection,
     Collection,
     ConfigService,
     ConfigService,
+    EntityRelationPaths,
     FacetValue,
     FacetValue,
     ID,
     ID,
     LanguageCode,
     LanguageCode,
@@ -42,7 +43,7 @@ import {
     VariantIndexItem,
     VariantIndexItem,
 } from './types';
 } from './types';
 
 
-export const productRelations = [
+export const defaultProductRelations: Array<EntityRelationPaths<Product>> = [
     'variants',
     'variants',
     'featuredAsset',
     'featuredAsset',
     'facetValues',
     'facetValues',
@@ -51,7 +52,7 @@ export const productRelations = [
     'channels.defaultTaxZone',
     'channels.defaultTaxZone',
 ];
 ];
 
 
-export const variantRelations = [
+export const defaultVariantRelations: Array<EntityRelationPaths<ProductVariant>> = [
     'featuredAsset',
     'featuredAsset',
     'facetValues',
     'facetValues',
     'facetValues.facet',
     'facetValues.facet',
@@ -76,6 +77,8 @@ type BulkVariantOperation = {
 export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDestroy {
 export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDestroy {
     private client: Client;
     private client: Client;
     private asyncQueue = new AsyncQueue('elasticsearch-indexer', 5);
     private asyncQueue = new AsyncQueue('elasticsearch-indexer', 5);
+    private productRelations: Array<EntityRelationPaths<Product>>;
+    private variantRelations: Array<EntityRelationPaths<ProductVariant>>;
 
 
     constructor(
     constructor(
         private connection: TransactionalConnection,
         private connection: TransactionalConnection,
@@ -88,6 +91,14 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
 
 
     onModuleInit(): any {
     onModuleInit(): any {
         this.client = getClient(this.options);
         this.client = getClient(this.options);
+        this.productRelations = this.getReindexRelationsRelations(
+            defaultProductRelations,
+            this.options.hydrateProductRelations,
+        );
+        this.variantRelations = this.getReindexRelationsRelations(
+            defaultVariantRelations,
+            this.options.hydrateProductVariantRelations,
+        );
     }
     }
 
 
     onModuleDestroy(): any {
     onModuleDestroy(): any {
@@ -442,22 +453,24 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
 
 
         for (const productId of productIds) {
         for (const productId of productIds) {
             operations.push(...(await this.deleteProductOperations(productId)));
             operations.push(...(await this.deleteProductOperations(productId)));
-            const optionsProductRelations = this.options.additionalProductRelationsToFetchFromDB ?
-                this.options.additionalProductRelationsToFetchFromDB: [];
-            const optionsVariantRelations = this.options.additionalVariantRelationsToFetchFromDB ?
-                this.options.additionalVariantRelationsToFetchFromDB: [];
-
-            const product = await this.connection.getRepository(Product).findOne(productId, {
-                relations: [...productRelations,...optionsProductRelations],
-                where: {
-                    deletedAt: null,
-                },
-            });
+
+            let product: Product | undefined;
+            try {
+                product = await this.connection.getRepository(Product).findOne(productId, {
+                    relations: this.productRelations,
+                    where: {
+                        deletedAt: null,
+                    },
+                });
+            } catch (e) {
+                Logger.error(e.message, loggerCtx, e.stack);
+                throw e;
+            }
             if (product) {
             if (product) {
                 const updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
                 const updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
                     product.variants.map(v => v.id),
                     product.variants.map(v => v.id),
                     {
                     {
-                        relations: [...variantRelations,...optionsVariantRelations],
+                        relations: this.variantRelations,
                         where: {
                         where: {
                             deletedAt: null,
                             deletedAt: null,
                         },
                         },
@@ -466,7 +479,8 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         },
                         },
                     },
                     },
                 );
                 );
-                updatedProductVariants.forEach(variant => (variant.product = product));
+                // tslint:disable-next-line:no-non-null-assertion
+                updatedProductVariants.forEach(variant => (variant.product = product!));
                 if (!product.enabled) {
                 if (!product.enabled) {
                     updatedProductVariants.forEach(v => (v.enabled = false));
                     updatedProductVariants.forEach(v => (v.enabled = false));
                 }
                 }
@@ -557,6 +571,35 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return operations;
         return operations;
     }
     }
 
 
+    /**
+     * Takes the default relations, and combines them with any extra relations specified in the
+     * `hydrateProductRelations` and `hydrateProductVariantRelations`. This method also ensures
+     * that the relation values are unique and that paths are fully expanded.
+     *
+     * This means that if a `hydrateProductRelations` value of `['assets.asset']` is specified,
+     * this method will also add `['assets']` to the relations array, otherwise TypeORM would
+     * throw an error trying to join a 2nd-level deep relation without the first level also
+     * being joined.
+     */
+    private getReindexRelationsRelations<T extends Product | ProductVariant>(
+        defaultRelations: Array<EntityRelationPaths<T>>,
+        hydratedRelations: Array<EntityRelationPaths<T>>,
+    ): Array<EntityRelationPaths<T>> {
+        const uniqueRelations = unique([...defaultRelations, ...hydratedRelations]);
+        for (const relation of hydratedRelations) {
+            const path = relation.split('.');
+            const pathToPart: string[] = [];
+            for (const part of path) {
+                pathToPart.push(part);
+                const joinedPath = pathToPart.join('.') as EntityRelationPaths<T>;
+                if (!uniqueRelations.includes(joinedPath)) {
+                    uniqueRelations.push(joinedPath);
+                }
+            }
+        }
+        return uniqueRelations;
+    }
+
     private async deleteProductOperations(productId: ID): Promise<BulkVariantOperation[]> {
     private async deleteProductOperations(productId: ID): Promise<BulkVariantOperation[]> {
         const channels = await this.connection
         const channels = await this.connection
             .getRepository(Channel)
             .getRepository(Channel)
@@ -728,7 +771,9 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                 productVariantName: variantTranslation.name,
                 productVariantName: variantTranslation.name,
                 productVariantAssetId: variantAsset ? variantAsset.id : undefined,
                 productVariantAssetId: variantAsset ? variantAsset.id : undefined,
                 productVariantPreview: variantAsset ? variantAsset.preview : '',
                 productVariantPreview: variantAsset ? variantAsset.preview : '',
-                productVariantPreviewFocalPoint: variantAsset ? variantAsset.focalPoint || undefined : undefined,
+                productVariantPreviewFocalPoint: variantAsset
+                    ? variantAsset.focalPoint || undefined
+                    : undefined,
                 price: v.price,
                 price: v.price,
                 priceWithTax: v.priceWithTax,
                 priceWithTax: v.priceWithTax,
                 currencyCode: v.currencyCode,
                 currencyCode: v.currencyCode,
@@ -767,8 +812,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                 item[`product-${name}`] = def.valueFn(v.product, variants, languageCode);
                 item[`product-${name}`] = def.valueFn(v.product, variants, languageCode);
             }
             }
             return item;
             return item;
-        }
-        catch (err) {
+        } catch (err) {
             Logger.error(err.toString());
             Logger.error(err.toString());
             throw Error(`Error while reindexing!`);
             throw Error(`Error while reindexing!`);
         }
         }

+ 33 - 8
packages/elasticsearch-plugin/src/options.ts

@@ -1,5 +1,5 @@
 import { ClientOptions } from '@elastic/elasticsearch';
 import { ClientOptions } from '@elastic/elasticsearch';
-import { DeepRequired, ID, LanguageCode, Product, ProductVariant } from '@vendure/core';
+import { DeepRequired, EntityRelationPaths, ID, LanguageCode, Product, ProductVariant } from '@vendure/core';
 import deepmerge from 'deepmerge';
 import deepmerge from 'deepmerge';
 
 
 import { CustomMapping, ElasticSearchInput } from './types';
 import { CustomMapping, ElasticSearchInput } from './types';
@@ -137,7 +137,8 @@ export interface ElasticsearchOptions {
      * Elasticsearch index and expose that data via the SearchResult GraphQL type,
      * Elasticsearch index and expose that data via the SearchResult GraphQL type,
      * adding a new `customMappings` field.
      * adding a new `customMappings` field.
      *
      *
-     * The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean` and
+     * The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, or list
+     * versions thereof (`[String!]` etc) and
      * can be appended with a `!` to indicate non-nullable fields.
      * can be appended with a `!` to indicate non-nullable fields.
      *
      *
      * This config option defines custom mappings which are accessible when the "groupByProduct"
      * This config option defines custom mappings which are accessible when the "groupByProduct"
@@ -209,14 +210,38 @@ export interface ElasticsearchOptions {
     bufferUpdates?: boolean;
     bufferUpdates?: boolean;
     /**
     /**
      * @description
      * @description
-     * Additional product relations that will be fetched from DB while reindexing.
+     * Additional product relations that will be fetched from DB while reindexing. This can be used
+     * in combination with `customProductMappings` to ensure that the required relations are joined
+     * before the `product` object is passed to the `valueFn`.
+     *
+     * @example
+     * ```TypeScript
+     * {
+     *   hydrateProductRelations: ['assets.asset'],
+     *   customProductMappings: {
+     *     assetPreviews: {
+     *       graphQlType: '[String!]',
+     *       // Here we can be sure that the `product.assets` array is populated
+     *       // with an Asset object
+     *       valueFn: (product) => product.assets.map(asset => asset.preview),
+     *     }
+     *   }
+     * }
+     * ```
+     *
+     * @default []
+     * @since 1.3.0
      */
      */
-    additionalProductRelationsToFetchFromDB?: [string]|[];
+    hydrateProductRelations?: Array<EntityRelationPaths<Product>>;
     /**
     /**
      * @description
      * @description
-     * Additional variant relations that will be fetched from DB while reindexing.
+     * Additional variant relations that will be fetched from DB while reindexing. See
+     * `hydrateProductRelations` for more explanation and a usage example.
+     *
+     * @default []
+     * @since 1.3.0
      */
      */
-    additionalVariantRelationsToFetchFromDB?: [string]|[];
+    hydrateProductVariantRelations?: Array<EntityRelationPaths<ProductVariant>>;
 }
 }
 
 
 /**
 /**
@@ -431,8 +456,8 @@ export const defaultOptions: ElasticsearchRuntimeOptions = {
     customProductMappings: {},
     customProductMappings: {},
     customProductVariantMappings: {},
     customProductVariantMappings: {},
     bufferUpdates: false,
     bufferUpdates: false,
-    additionalProductRelationsToFetchFromDB:[],
-    additionalVariantRelationsToFetchFromDB:[],
+    hydrateProductRelations: [],
+    hydrateProductVariantRelations: [],
 };
 };
 
 
 export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
 export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {