Browse Source

feat(core): Add custom validation function to custom field config

Relates to #85
Michael Bromley 6 years ago
parent
commit
80eba9dbd8

+ 48 - 0
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -4,6 +4,8 @@ process.env.TZ = 'UTC';
 import gql from 'graphql-tag';
 import path from 'path';
 
+import { LanguageCode } from '../../common/lib/generated-types';
+
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
@@ -47,6 +49,18 @@ describe('Custom fields', () => {
                             min: '2019-01-01T08:30',
                             max: '2019-06-01T08:30',
                         },
+                        { name: 'validateFn1', type: 'string', validate: value => {
+                                if (value !== 'valid') {
+                                    return `The value ['${value}'] is not valid`;
+                                }
+                            },
+                        },
+                        { name: 'validateFn2', type: 'string', validate: value => {
+                                if (value !== 'valid') {
+                                    return [{ languageCode: LanguageCode.en, value: `The value ['${value}'] is not valid` }];
+                                }
+                            },
+                        },
                     ],
                 },
             },
@@ -92,6 +106,8 @@ describe('Custom fields', () => {
                 { name: 'validateInt', type: 'int' },
                 { name: 'validateFloat', type: 'float' },
                 { name: 'validateDateTime', type: 'datetime' },
+                { name: 'validateFn1', type: 'string' },
+                { name: 'validateFn2', type: 'string' },
             ],
         });
     });
@@ -244,5 +260,37 @@ describe('Custom fields', () => {
                 `);
             }, `The custom field value [2019-01-01T05:25:00.000Z] is less than the minimum [2019-01-01T08:30]`),
         );
+
+        it(
+            'invalid validate function with string',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query(gql`
+                    mutation {
+                        updateProduct(input: {
+                            id: "T_1"
+                            customFields: { validateFn1: "invalid" }
+                        }) {
+                            id
+                        }
+                    }
+                `);
+            }, `The value ['invalid'] is not valid`),
+        );
+
+        it(
+            'invalid validate function with localized string',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query(gql`
+                    mutation {
+                        updateProduct(input: {
+                            id: "T_1"
+                            customFields: { validateFn2: "invalid" }
+                        }) {
+                            id
+                        }
+                    }
+                `);
+            }, `The value ['invalid'] is not valid`),
+        );
     });
 });

+ 47 - 0
packages/core/src/api/common/validate-custom-field-value.spec.ts

@@ -1,3 +1,5 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+
 import { validateCustomFieldValue } from './validate-custom-field-value';
 
 describe('validateCustomFieldValue()', () => {
@@ -65,4 +67,49 @@ describe('validateCustomFieldValue()', () => {
             expect(validate('2019-06-01T08:30:00.100')).toThrowError('error.field-invalid-datetime-range-max');
         });
     });
+
+    describe('validate function', () => {
+
+        const validate1 = (value: string) => () => validateCustomFieldValue({
+            name: 'test',
+            type: 'string',
+            validate: v => {
+                if (v !== 'valid') {
+                    return 'invalid';
+                }
+            },
+        }, value);
+        const validate2 = (value: string, languageCode: LanguageCode) => () => validateCustomFieldValue({
+            name: 'test',
+            type: 'string',
+            validate: v => {
+                if (v !== 'valid') {
+                    return [
+                        { languageCode: LanguageCode.en, value: 'invalid' },
+                        { languageCode: LanguageCode.de, value: 'ungültig' },
+                    ];
+                }
+            },
+        }, value, languageCode);
+
+        it('passes validate fn string', () => {
+            expect(validate1('valid')).not.toThrow();
+        });
+
+        it('passes validate fn localized string', () => {
+            expect(validate2('valid', LanguageCode.de)).not.toThrow();
+        });
+
+        it('fails validate fn string', () => {
+            expect(validate1('bad')).toThrowError('invalid');
+        });
+
+        it('fails validate fn localized string en', () => {
+            expect(validate2('bad', LanguageCode.en)).toThrowError('invalid');
+        });
+
+        it('fails validate fn localized string de', () => {
+            expect(validate2('bad', LanguageCode.de)).toThrowError('ungültig');
+        });
+    });
 });

+ 21 - 4
packages/core/src/api/common/validate-custom-field-value.ts

@@ -1,5 +1,7 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 
+import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { UserInputError } from '../../common/error/errors';
 import {
     CustomFieldConfig,
@@ -8,30 +10,45 @@ import {
     IntCustomFieldConfig,
     LocaleStringCustomFieldConfig,
     StringCustomFieldConfig,
+    TypedCustomFieldConfig,
 } from '../../config/custom-field/custom-field-types';
 
 /**
  * Validates the value of a custom field input against any configured constraints.
  * If validation fails, an error is thrown.
  */
-export function validateCustomFieldValue(config: CustomFieldConfig, value: any): void {
+export function validateCustomFieldValue(config: CustomFieldConfig, value: any, languageCode?: LanguageCode): void {
     switch (config.type) {
         case 'string':
         case 'localeString':
-            return validateStringField(config, value);
+            validateStringField(config, value);
             break;
         case 'int':
         case 'float':
-            return validateNumberField(config, value);
+            validateNumberField(config, value);
             break;
         case 'datetime':
-            return validateDateTimeField(config, value);
+            validateDateTimeField(config, value);
             break;
         case 'boolean':
             break;
         default:
             assertNever(config);
     }
+    validateCustomFunction(config, value, languageCode);
+}
+
+function validateCustomFunction<T extends TypedCustomFieldConfig<any, any>>(config: T, value: any, languageCode?: LanguageCode) {
+    if (typeof config.validate === 'function') {
+        const error = config.validate(value);
+        if (typeof error === 'string') {
+            throw new UserInputError(error);
+        }
+        if (Array.isArray(error)) {
+            const localizedError = error.find(e => e.languageCode === (languageCode || DEFAULT_LANGUAGE_CODE)) || error[0];
+            throw new UserInputError(localizedError.value);
+        }
+    }
 }
 
 function validateStringField(config: StringCustomFieldConfig | LocaleStringCustomFieldConfig, value: string): void {

+ 14 - 10
packages/core/src/api/middleware/validate-custom-fields-interceptor.ts

@@ -1,5 +1,7 @@
 import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
 import { GqlExecutionContext } from '@nestjs/graphql';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { assertNever } from '@vendure/common/lib/shared-utils';
 import {
     DefinitionNode,
     GraphQLInputType,
@@ -11,7 +13,6 @@ import {
     TypeNode,
 } from 'graphql';
 
-import { assertNever } from '../../../../common/lib/shared-utils';
 import { UserInputError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import {
@@ -20,6 +21,8 @@ import {
     LocaleStringCustomFieldConfig,
     StringCustomFieldConfig,
 } from '../../config/custom-field/custom-field-types';
+import { RequestContext } from '../common/request-context';
+import { REQUEST_CONTEXT_KEY } from '../common/request-context.service';
 import { validateCustomFieldValue } from '../common/validate-custom-field-value';
 
 /**
@@ -40,16 +43,17 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
     }
 
     intercept(context: ExecutionContext, next: CallHandler<any>) {
-        const ctx = GqlExecutionContext.create(context);
-        const { operation, schema } = ctx.getInfo<GraphQLResolveInfo>();
-        const variables = ctx.getArgs();
+        const gqlExecutionContext = GqlExecutionContext.create(context);
+        const { operation, schema } = gqlExecutionContext.getInfo<GraphQLResolveInfo>();
+        const variables = gqlExecutionContext.getArgs();
+        const ctx: RequestContext = gqlExecutionContext.getContext().req[REQUEST_CONTEXT_KEY];
 
         if (operation.operation === 'mutation') {
             const inputTypeNames = this.getArgumentMap(operation, schema);
             Object.entries(inputTypeNames).forEach(([inputName, typeName]) => {
                 if (this.inputsWithCustomFields.has(typeName)) {
                     if (variables[inputName]) {
-                        this.validateInput(typeName, variables[inputName]);
+                        this.validateInput(typeName, ctx.languageCode, variables[inputName]);
                     }
                 }
             });
@@ -57,19 +61,19 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
         return next.handle();
     }
 
-    private validateInput(typeName: string, variableValues?: { [key: string]: any }) {
+    private validateInput(typeName: string, languageCode: LanguageCode, variableValues?: { [key: string]: any }) {
         if (variableValues) {
             const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
             const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
             if (customFieldConfig) {
                 if (variableValues.customFields) {
-                    this.validateCustomFieldsObject(customFieldConfig, variableValues.customFields);
+                    this.validateCustomFieldsObject(customFieldConfig, languageCode, variableValues.customFields);
                 }
                 const translations = variableValues.translations;
                 if (Array.isArray(translations)) {
                     for (const translation of translations) {
                         if (translation.customFields) {
-                            this.validateCustomFieldsObject(customFieldConfig, translation.customFields);
+                            this.validateCustomFieldsObject(customFieldConfig, languageCode, translation.customFields);
                         }
                     }
                 }
@@ -77,11 +81,11 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
         }
     }
 
-    private validateCustomFieldsObject(customFieldConfig: CustomFieldConfig[], customFieldsObject: { [key: string]: any; }) {
+    private validateCustomFieldsObject(customFieldConfig: CustomFieldConfig[], languageCode: LanguageCode, customFieldsObject: { [key: string]: any; }) {
         for (const [key, value] of Object.entries(customFieldsObject)) {
             const config = customFieldConfig.find(c => c.name === key);
             if (config) {
-                validateCustomFieldValue(config, value);
+                validateCustomFieldValue(config, value, languageCode);
             }
         }
     }

+ 4 - 2
packages/core/src/config/custom-field/custom-field-types.ts

@@ -1,10 +1,11 @@
-import { BooleanCustomFieldConfig as GraphQLBooleanCustomFieldConfig,
+import {
+    BooleanCustomFieldConfig as GraphQLBooleanCustomFieldConfig,
     CustomField,
-    CustomFieldConfig as GraphQLCustomFieldConfig,
     DateTimeCustomFieldConfig as GraphQLDateTimeCustomFieldConfig,
     FloatCustomFieldConfig as GraphQLFloatCustomFieldConfig,
     IntCustomFieldConfig as GraphQLIntCustomFieldConfig,
     LocaleStringCustomFieldConfig as GraphQLLocaleStringCustomFieldConfig,
+    LocalizedString,
     StringCustomFieldConfig as GraphQLStringCustomFieldConfig,
 } from '@vendure/common/lib/generated-types';
 import { CustomFieldsObject, CustomFieldType } from '@vendure/common/src/shared-types';
@@ -29,6 +30,7 @@ export type TypedCustomFieldConfig<T extends CustomFieldType, C extends CustomFi
     type: T;
     defaultValue?: DefaultValueType<T>;
     nullable?: boolean;
+    validate?: (value: DefaultValueType<T>) => string | LocalizedString[] | void;
 };
 export type StringCustomFieldConfig = TypedCustomFieldConfig<'string', GraphQLStringCustomFieldConfig>;
 export type LocaleStringCustomFieldConfig = TypedCustomFieldConfig<'localeString', GraphQLLocaleStringCustomFieldConfig>;