1
0
Эх сурвалжийг харах

feat(elasticsearch-plugin): Allow full client options to be passed

Closes #474
Michael Bromley 5 жил өмнө
parent
commit
c686509ba8

+ 1 - 1
packages/elasticsearch-plugin/README.md

@@ -1,6 +1,6 @@
 # Vendure Elasticsearch Plugin
 
-The `ElasticsearchPlugin` uses Elasticsearch to power the the Vendure product search. 
+The `ElasticsearchPlugin` uses Elasticsearch to power the Vendure product search. 
 
 **Requires Elasticsearch v7.0 or higher.** 
 

+ 12 - 8
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -1,4 +1,4 @@
-import { Client } from '@elastic/elasticsearch';
+import { Client, ClientOptions } from '@elastic/elasticsearch';
 import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
 import { SearchResult, SearchResultAsset } from '@vendure/common/lib/generated-types';
 import {
@@ -50,8 +50,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
 
     onModuleInit(): any {
         const { host, port } = this.options;
+        const node = this.options.clientOptions?.node ?? `${host}:${port}`;
         this.client = new Client({
-            node: `${host}:${port}`,
+            node,
+            // `any` cast is there due to a strange error "Property '[Symbol.iterator]' is missing in type... URLSearchParams"
+            // which looks like possibly a TS/definitions bug.
+            ...(this.options.clientOptions as any),
         });
     }
 
@@ -105,7 +109,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
                 body: elasticSearchBody,
             });
             return {
-                items: body.hits.hits.map((hit) => this.mapProductToSearchResult(hit)),
+                items: body.hits.hits.map(hit => this.mapProductToSearchResult(hit)),
                 totalItems: body.hits.total.value,
             };
         } else {
@@ -115,7 +119,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
                 body: elasticSearchBody,
             });
             return {
-                items: body.hits.hits.map((hit) => this.mapVariantToSearchResult(hit)),
+                items: body.hits.hits.map(hit => this.mapVariantToSearchResult(hit)),
                 totalItems: body.hits.total.value,
             };
         }
@@ -155,11 +159,11 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         const buckets = body.aggregations ? body.aggregations.facetValue.buckets : [];
 
         const facetValues = await this.facetValueService.findByIds(
-            buckets.map((b) => b.key),
+            buckets.map(b => b.key),
             ctx.languageCode,
         );
         return facetValues.map((facetValue, index) => {
-            const bucket = buckets.find((b) => b.key.toString() === facetValue.id.toString());
+            const bucket = buckets.find(b => b.key.toString() === facetValue.id.toString());
             return {
                 facetValue,
                 count: bucket ? bucket.doc_count : 0,
@@ -233,8 +237,8 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
                 min: aggregations.minPriceWithTax.value || 0,
                 max: aggregations.maxPriceWithTax.value || 0,
             },
-            buckets: aggregations.prices.buckets.map(mapPriceBuckets).filter((x) => 0 < x.count),
-            bucketsWithTax: aggregations.prices.buckets.map(mapPriceBuckets).filter((x) => 0 < x.count),
+            buckets: aggregations.prices.buckets.map(mapPriceBuckets).filter(x => 0 < x.count),
+            bucketsWithTax: aggregations.prices.buckets.map(mapPriceBuckets).filter(x => 0 < x.count),
         };
     }
 

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

@@ -1,3 +1,4 @@
+import { ClientOptions } from '@elastic/elasticsearch';
 import { DeepRequired, ID, Product, ProductVariant } from '@vendure/core';
 import deepmerge from 'deepmerge';
 
@@ -13,14 +14,26 @@ import { CustomMapping, ElasticSearchInput } from './types';
 export interface ElasticsearchOptions {
     /**
      * @description
-     * The host of the Elasticsearch server.
+     * The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
+     *
+     * @default 'http://localhost'
      */
-    host: string;
+    host?: string;
     /**
      * @description
-     * The port of the Elasticsearch server.
+     * The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
+     *
+     * @default 9200
      */
-    port: number;
+    port?: number;
+    /**
+     * @description
+     * Options to pass directly to the
+     * [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
+     * set authentication or other more advanced options.
+     * Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
+     */
+    clientOptions?: ClientOptions;
     /**
      * @description
      * Prefix for the indices created by the plugin.
@@ -275,7 +288,11 @@ export interface BoostFieldsConfig {
     sku?: number;
 }
 
-export const defaultOptions: DeepRequired<ElasticsearchOptions> = {
+export type ElasticsearchRuntimeOptions = DeepRequired<Omit<ElasticsearchOptions, 'clientOptions'>> & {
+    clientOptions?: ClientOptions;
+};
+
+export const defaultOptions: ElasticsearchRuntimeOptions = {
     host: 'http://localhost',
     port: 9200,
     indexPrefix: 'vendure-',
@@ -290,12 +307,14 @@ export const defaultOptions: DeepRequired<ElasticsearchOptions> = {
             sku: 1,
         },
         priceRangeBucketInterval: 1000,
-        mapQuery: (query) => query,
+        mapQuery: query => query,
     },
     customProductMappings: {},
     customProductVariantMappings: {},
 };
 
-export function mergeWithDefaults(userOptions: ElasticsearchOptions): DeepRequired<ElasticsearchOptions> {
-    return deepmerge(defaultOptions, userOptions) as DeepRequired<ElasticsearchOptions>;
+export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
+    const { clientOptions, ...pluginOptions } = userOptions;
+    const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions;
+    return { ...merged, clientOptions };
 }

+ 36 - 8
packages/elasticsearch-plugin/src/plugin.ts

@@ -1,7 +1,7 @@
+import { NodeOptions } from '@elastic/elasticsearch';
 import {
     AssetEvent,
     CollectionModificationEvent,
-    DeepRequired,
     EventBus,
     HealthCheckRegistryService,
     ID,
@@ -26,7 +26,7 @@ import { ElasticsearchHealthIndicator } from './elasticsearch.health';
 import { ElasticsearchService } from './elasticsearch.service';
 import { generateSchemaExtensions } from './graphql-schema-extensions';
 import { ElasticsearchIndexerController } from './indexer.controller';
-import { ElasticsearchOptions, mergeWithDefaults } from './options';
+import { ElasticsearchOptions, ElasticsearchRuntimeOptions, mergeWithDefaults } from './options';
 
 /**
  * @description
@@ -37,11 +37,11 @@ import { ElasticsearchOptions, mergeWithDefaults } from './options';
  *
  * **Requires Elasticsearch v7.0 or higher.**
  *
- * `yarn add \@vendure/elasticsearch-plugin`
+ * `yarn add \@elastic/elasticsearch \@vendure/elasticsearch-plugin`
  *
  * or
  *
- * `npm install \@vendure/elasticsearch-plugin`
+ * `npm install \@elastic/elasticsearch \@vendure/elasticsearch-plugin`
  *
  * Make sure to remove the `DefaultSearchPlugin` if it is still in the VendureConfig plugins array.
  *
@@ -208,12 +208,14 @@ import { ElasticsearchOptions, mergeWithDefaults } from './options';
                 ? [ShopElasticSearchResolver, CustomMappingsResolver]
                 : [ShopElasticSearchResolver];
         },
-        schema: () => generateSchemaExtensions(ElasticsearchPlugin.options),
+        // `any` cast is there due to a strange error "Property '[Symbol.iterator]' is missing in type... URLSearchParams"
+        // which looks like possibly a TS/definitions bug.
+        schema: () => generateSchemaExtensions(ElasticsearchPlugin.options as any),
     },
     workers: [ElasticsearchIndexerController],
 })
 export class ElasticsearchPlugin implements OnVendureBootstrap {
-    private static options: DeepRequired<ElasticsearchOptions>;
+    private static options: ElasticsearchRuntimeOptions;
 
     /** @internal */
     constructor(
@@ -235,17 +237,18 @@ export class ElasticsearchPlugin implements OnVendureBootstrap {
     /** @internal */
     async onVendureBootstrap(): Promise<void> {
         const { host, port } = ElasticsearchPlugin.options;
+        const nodeName = this.nodeName();
         try {
             const pingResult = await this.elasticsearchService.checkConnection();
         } catch (e) {
-            Logger.error(`Could not connect to Elasticsearch instance at "${host}:${port}"`, loggerCtx);
+            Logger.error(`Could not connect to Elasticsearch instance at "${nodeName}"`, loggerCtx);
             Logger.error(JSON.stringify(e), loggerCtx);
             this.healthCheckRegistryService.registerIndicatorFunction(() =>
                 this.elasticsearchHealthIndicator.startupCheckFailed(e.message),
             );
             return;
         }
-        Logger.info(`Sucessfully connected to Elasticsearch instance at "${host}:${port}"`, loggerCtx);
+        Logger.info(`Successfully connected to Elasticsearch instance at "${nodeName}"`, loggerCtx);
 
         await this.elasticsearchService.createIndicesIfNotExists();
         this.elasticsearchIndexService.initJobQueue();
@@ -315,4 +318,29 @@ export class ElasticsearchPlugin implements OnVendureBootstrap {
             }
         });
     }
+
+    /**
+     * Returns a string representation of the target node(s) that the Elasticsearch
+     * client is configured to connect to.
+     */
+    private nodeName(): string {
+        const { host, port, clientOptions } = ElasticsearchPlugin.options;
+        const node = clientOptions?.node;
+        const nodes = clientOptions?.nodes;
+        if (nodes) {
+            return [...nodes].join(', ');
+        }
+        if (node) {
+            if (Array.isArray(node)) {
+                return (node as any[])
+                    .map((n: string | NodeOptions) => {
+                        return typeof n === 'string' ? n : n.url.toString();
+                    })
+                    .join(', ');
+            } else {
+                return typeof node === 'string' ? node : node.url.toString();
+            }
+        }
+        return `${host}:${port}`;
+    }
 }