Browse Source

refactor(elasticsearch-plugin): Rename field, improve type defs and docs

Michael Bromley 4 years ago
parent
commit
749c452078

+ 1 - 1
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -143,7 +143,7 @@ describe('Elasticsearch plugin', () => {
                         scriptFields: {
                             answerDouble: {
                                 graphQlType: 'Int!',
-                                environment: 'product',
+                                context: 'product',
                                 scriptFn: input => ({
                                     script: `doc['product-answer'].value * 2`,
                                 }),

+ 3 - 3
packages/elasticsearch-plugin/src/build-elastic-body.ts

@@ -165,13 +165,13 @@ function createScriptFields(
             const result: any = {};
             for (const name of fields) {
                 const scriptField = scriptFields[name];
-                if (scriptField.environment === 'product' && groupByProduct === true) {
+                if (scriptField.context === 'product' && groupByProduct === true) {
                     (result as any)[name] = scriptField.scriptFn(input);
                 }
-                if (scriptField.environment === 'variant' && groupByProduct === false) {
+                if (scriptField.context === 'variant' && groupByProduct === false) {
                     (result as any)[name] = scriptField.scriptFn(input);
                 }
-                if (scriptField.environment === 'both' || scriptField.environment === undefined) {
+                if (scriptField.context === 'both' || scriptField.context === undefined) {
                     (result as any)[name] = scriptField.scriptFn(input);
                 }
             }

+ 1 - 1
packages/elasticsearch-plugin/src/custom-script-fields.resolver.ts

@@ -17,7 +17,7 @@ export class CustomScriptFieldsResolver {
     @ResolveField()
     __resolveType(value: any): string {
         const productScriptFields = Object.entries(this.options.searchConfig?.scriptFields || {})
-            .filter(([, scriptField]) => scriptField.environment !== 'variant')
+            .filter(([, scriptField]) => scriptField.context !== 'variant')
             .map(([k]) => k);
         return Object.keys(value).every(k => productScriptFields.includes(k))
             ? 'CustomProductScriptFields'

+ 3 - 3
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -23,7 +23,7 @@ import { createIndices, getClient } from './indexing-utils';
 import { ElasticsearchOptions } from './options';
 import {
     CustomMapping,
-    CustomScriptEnvironment,
+    CustomScriptContext,
     CustomScriptMapping,
     ElasticSearchInput,
     ElasticSearchResponse,
@@ -596,13 +596,13 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
         result: any,
         fields: any,
         mappings: { [fieldName: string]: CustomScriptMapping<any> },
-        environment: CustomScriptEnvironment,
+        environment: CustomScriptContext,
     ): any {
         const customMappings = Object.keys(mappings || {});
         if (customMappings.length) {
             const customScriptFieldsResult: any = {};
             for (const name of customMappings) {
-                const env = mappings[name].environment;
+                const env = mappings[name].context;
                 if (env === environment || env === 'both') {
                     const fieldVal = (fields as any)[name] || undefined;
                     if (Array.isArray(fieldVal)) {

+ 2 - 2
packages/elasticsearch-plugin/src/graphql-schema-extensions.ts

@@ -45,10 +45,10 @@ function generateCustomMappingTypes(options: ElasticsearchOptions): DocumentNode
     const productMappings = Object.entries(options.customProductMappings || {});
     const variantMappings = Object.entries(options.customProductVariantMappings || {});
     const scriptProductFields = Object.entries(options.searchConfig?.scriptFields || {}).filter(
-        ([, scriptField]) => scriptField.environment !== 'variant',
+        ([, scriptField]) => scriptField.context !== 'variant',
     );
     const scriptVariantFields = Object.entries(options.searchConfig?.scriptFields || {}).filter(
-        ([, scriptField]) => scriptField.environment !== 'product',
+        ([, scriptField]) => scriptField.context !== 'product',
     );
     let sdl = ``;
 

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

@@ -113,11 +113,31 @@ export interface ElasticsearchOptions {
      * }
      * ```
      *
+     * To reference a field defined by `customProductMappings` or `customProductVariantMappings`, you will
+     * need to prefix the name with `'product-<name>'` or `'variant-<name>'` respectively, e.g.:
+     *
+     * @example
+     * ```TypeScript
+     * customProductMappings: {
+     *    variantCount: {
+     *        graphQlType: 'Int!',
+     *        valueFn: (product, variants) => variants.length,
+     *    },
+     * },
+     * indexMappingProperties: {
+     *   'product-variantCount': {
+     *     type: 'integer',
+     *   }
+     * }
+     * ```
+     *
      * @since 1.2.0
      * @default
      * {}
      */
-    indexMappingProperties?: object;
+    indexMappingProperties?: {
+        [indexName: string]: object;
+    };
     /**
      * @description
      * Batch size for bulk operations (e.g. when rebuilding the indices).
@@ -128,7 +148,7 @@ export interface ElasticsearchOptions {
     batchSize?: number;
     /**
      * @description
-     * Configuration of the internal Elasticseach query.
+     * Configuration of the internal Elasticsearch query.
      */
     searchConfig?: SearchConfig;
     /**
@@ -398,39 +418,47 @@ export interface SearchConfig {
     ) => any;
     /**
      * @description
-     * Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit
-     * @since 1.3.0
+     * Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
+     *
      * @example
      * ```TypeScript
      * indexMappingProperties: {
-     *      location: {
-     *          type: 'geo_point', // contains function arcDistance
-     *      },
+     *   // The `product-location` field corresponds to the `location` customProductMapping
+     *   // defined below. Here we specify that it would be index as a `geo_point` type,
+     *   // which will allow us to perform geo-spacial calculations on it in our script field.
+     *   'product-location': {
+     *     type: 'geo_point', // contains function arcDistance
+     *   },
      * },
      * customProductMappings: {
-     *      location: {
-     *          graphQlType: 'String',
-     *          valueFn: (product: Product) => {
-     *              const custom = product.customFields.location;
-     *              return `${custom.latitude},${location.longitude}`;
-     *          },
-     *      }
+     *   location: {
+     *     graphQlType: 'String',
+     *     valueFn: (product: Product) => {
+     *       const custom = product.customFields.location;
+     *       return `${custom.latitude},${custom.longitude}`;
+     *     },
+     *   }
      * },
-     * scriptFields: {
-     *      distance: {
-     *          graphQlType: 'Int',
-     *          environment: 'product',
-     *          scriptFn: (input) => {
-     *              // assuming SearchInput was extended with latitude and longitude
-     *              const lat = input.latitude;
-     *              const lon = input.longitude;
-     *              return {
-     *                  script: `doc['product-location'].arcDistance(${lat}, ${lon})`,
-     *              }
-     *          }
-     *      }
+     * searchConfig: {
+     *   scriptFields: {
+     *     distance: {
+     *       graphQlType: 'Float!',
+     *       // Run this script only when grouping results by product
+     *       context: 'product',
+     *       scriptFn: (input) => {
+     *         // assuming SearchInput was extended with latitude and longitude
+     *         const lat = input.latitude;
+     *         const lon = input.longitude;
+     *         return {
+     *           script: `doc['product-location'].arcDistance(${lat}, ${lon})`,
+     *         }
+     *       }
+     *     }
+     *   }
      * }
      * ```
+     *
+     * @since 1.3.0
      */
     scriptFields?: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> };
 }

+ 2 - 2
packages/elasticsearch-plugin/src/plugin.ts

@@ -224,11 +224,11 @@ import { ElasticsearchOptions, ElasticsearchRuntimeOptions, mergeWithDefaults }
             const requiresUnionScriptResolver =
                 0 <
                     Object.values(options.searchConfig.scriptFields || {}).filter(
-                        field => field.environment !== 'product',
+                        field => field.context !== 'product',
                     ).length &&
                 0 <
                     Object.values(options.searchConfig.scriptFields || {}).filter(
-                        field => field.environment !== 'variant',
+                        field => field.context !== 'variant',
                     ).length;
             return [
                 ShopElasticSearchResolver,

+ 34 - 100
packages/elasticsearch-plugin/src/types.ts

@@ -214,11 +214,6 @@ export interface UpdateAssetMessageData {
 }
 
 type Maybe<T> = T | undefined;
-type CustomMappingDefinition<Args extends any[], T extends string, R> = {
-    graphQlType: T;
-    valueFn: (...args: Args) => R;
-};
-
 type NamedJobData<Type extends string, MessageData> = { type: Type } & MessageData;
 
 export type ReindexJobData = NamedJobData<'reindex', ReindexMessageData>;
@@ -247,103 +242,42 @@ export type UpdateIndexQueueJobData =
     | AssignVariantToChannelJobData
     | RemoveVariantFromChannelJobData;
 
-type CustomIdMapping<Args extends any[]> = CustomMappingDefinition<Args, 'ID!', ID>;
-type CustomIdMappingList<Args extends any[]> = CustomMappingDefinition<Args, '[ID!]!', ID[]>;
-type CustomIdMappingNullable<Args extends any[]> = CustomMappingDefinition<Args, 'ID', Maybe<ID>>;
-type CustomIdMappingNullableList<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    '[ID!]',
-    Array<Maybe<ID>>
->;
-type CustomStringMapping<Args extends any[]> = CustomMappingDefinition<Args, 'String!', string>;
-type CustomStringMappingList<Args extends any[]> = CustomMappingDefinition<Args, '[String!]!', string[]>;
-type CustomStringMappingNullable<Args extends any[]> = CustomMappingDefinition<Args, 'String', Maybe<string>>;
-type CustomStringMappingNullableList<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    '[String!]',
-    Array<Maybe<string>>
->;
-type CustomIntMapping<Args extends any[]> = CustomMappingDefinition<Args, 'Int!', number>;
-type CustomIntMappingList<Args extends any[]> = CustomMappingDefinition<Args, '[Int!]!', number[]>;
-type CustomIntMappingNullable<Args extends any[]> = CustomMappingDefinition<Args, 'Int', Maybe<number>>;
-type CustomIntMappingNullableList<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    '[Int!]',
-    Array<Maybe<number>>
->;
-type CustomFloatMapping<Args extends any[]> = CustomMappingDefinition<Args, 'Float!', number>;
-type CustomFloatMappingList<Args extends any[]> = CustomMappingDefinition<Args, '[Float!]!', number[]>;
-type CustomFloatMappingNullable<Args extends any[]> = CustomMappingDefinition<Args, 'Float', Maybe<number>>;
-type CustomFloatMappingNullableList<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    '[Float!]',
-    Array<Maybe<number>>
->;
-type CustomBooleanMapping<Args extends any[]> = CustomMappingDefinition<Args, 'Boolean!', boolean>;
-type CustomBooleanMappingList<Args extends any[]> = CustomMappingDefinition<Args, '[Boolean!]!', boolean[]>;
-type CustomBooleanMappingNullable<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    'Boolean',
-    Maybe<boolean>
->;
-type CustomBooleanMappingNullableList<Args extends any[]> = CustomMappingDefinition<
-    Args,
-    '[Boolean!]',
-    Array<Maybe<boolean>>
->;
-
-export type CustomMapping<Args extends any[]> =
-    | CustomIdMapping<Args>
-    | CustomIdMappingList<Args>
-    | CustomIdMappingNullable<Args>
-    | CustomIdMappingNullableList<Args>
-    | CustomStringMapping<Args>
-    | CustomStringMappingList<Args>
-    | CustomStringMappingNullable<Args>
-    | CustomStringMappingNullableList<Args>
-    | CustomIntMapping<Args>
-    | CustomIntMappingList<Args>
-    | CustomIntMappingNullable<Args>
-    | CustomIntMappingNullableList<Args>
-    | CustomFloatMapping<Args>
-    | CustomFloatMappingList<Args>
-    | CustomFloatMappingNullable<Args>
-    | CustomFloatMappingNullableList<Args>
-    | CustomBooleanMapping<Args>
-    | CustomBooleanMappingList<Args>
-    | CustomBooleanMappingNullable<Args>
-    | CustomBooleanMappingNullableList<Args>;
+type GraphQlPrimitive = 'ID' | 'String' | 'Int' | 'Float' | 'Boolean';
+type PrimitiveTypeVariations<T extends GraphQlPrimitive> = T | `${T}!` | `[${T}!]` | `[${T}!]!`;
+type GraphQlPermittedReturnType = PrimitiveTypeVariations<GraphQlPrimitive>;
 
-export type CustomScriptEnvironment = 'product' | 'variant' | 'both';
-type CustomScriptMappingDefinition<Args extends any[], T extends CustomMappingTypes, R> = {
+type CustomMappingDefinition<Args extends any[], T extends GraphQlPermittedReturnType, R> = {
     graphQlType: T;
-    environment: CustomScriptEnvironment;
-    scriptFn: (...args: Args) => R;
+    valueFn: (...args: Args) => R;
 };
 
-type CustomScriptStringMapping<Args extends any[]> = CustomScriptMappingDefinition<Args, 'String!', any>;
-type CustomScriptStringMappingNullable<Args extends any[]> = CustomScriptMappingDefinition<
-    Args,
-    'String',
-    any
->;
-type CustomScriptIntMapping<Args extends any[]> = CustomScriptMappingDefinition<Args, 'Int!', any>;
-type CustomScriptIntMappingNullable<Args extends any[]> = CustomScriptMappingDefinition<Args, 'Int', any>;
-type CustomScriptFloatMapping<Args extends any[]> = CustomScriptMappingDefinition<Args, 'Float!', any>;
-type CustomScriptFloatMappingNullable<Args extends any[]> = CustomScriptMappingDefinition<Args, 'Float', any>;
-type CustomScriptBooleanMapping<Args extends any[]> = CustomScriptMappingDefinition<Args, 'Boolean!', any>;
-type CustomScriptBooleanMappingNullable<Args extends any[]> = CustomScriptMappingDefinition<
-    Args,
-    'Boolean',
-    any
->;
+type TypeVariationMap<GqlType extends GraphQlPrimitive, TsType> = {
+    [Key in PrimitiveTypeVariations<GqlType>]: Key extends `[${string}!]!`
+        ? TsType[]
+        : Key extends `[${string}!]`
+        ? Maybe<TsType[]>
+        : Key extends `${string}!`
+        ? TsType
+        : Maybe<TsType>;
+};
+
+type GraphQlTypeMap = TypeVariationMap<'ID', ID> &
+    TypeVariationMap<'String', string> &
+    TypeVariationMap<'Int', number> &
+    TypeVariationMap<'Float', number> &
+    TypeVariationMap<'Boolean', boolean>;
+
+export type CustomMapping<Args extends any[]> = {
+    [Type in GraphQlPermittedReturnType]: CustomMappingDefinition<Args, Type, GraphQlTypeMap[Type]>;
+}[GraphQlPermittedReturnType];
+
+export type CustomScriptContext = 'product' | 'variant' | 'both';
+type CustomScriptMappingDefinition<Args extends any[], T extends GraphQlPermittedReturnType> = {
+    graphQlType: T;
+    context: CustomScriptContext;
+    scriptFn: (...args: Args) => { script: string };
+};
 
-export type CustomScriptMapping<Args extends any[]> =
-    | CustomScriptStringMapping<Args>
-    | CustomScriptStringMappingNullable<Args>
-    | CustomScriptIntMapping<Args>
-    | CustomScriptIntMappingNullable<Args>
-    | CustomScriptFloatMapping<Args>
-    | CustomScriptFloatMappingNullable<Args>
-    | CustomScriptBooleanMapping<Args>
-    | CustomScriptBooleanMappingNullable<Args>;
+export type CustomScriptMapping<Args extends any[]> = {
+    [Type in GraphQlPermittedReturnType]: CustomScriptMappingDefinition<Args, Type>;
+}[GraphQlPermittedReturnType];