Browse Source

feat(core): Pass RequestContext to custom field validate function

Closes #2408
Michael Bromley 2 years ago
parent
commit
2314ff6a39

+ 2 - 2
docs/docs/guides/developer-guide/custom-fields/index.md

@@ -515,7 +515,7 @@ const config = {
 
 #### validate
 
-<CustomFieldProperty required={false} type="(value: any) => string | LocalizedString[] | void" />
+<CustomFieldProperty required={false} type="(value: any, injector: Injector, ctx: RequestContext) => string | LocalizedString[] | void" />
 
 A custom validation function. If the value is valid, then the function should not return a value. If a string or LocalizedString array is returned, this is interpreted as an error message.
 
@@ -563,7 +563,7 @@ const config = {
                 name: 'partCode',
                 type: 'string',
                 // highlight-start
-                validate: async (value, injector) => {
+                validate: async (value, injector, ctx) => {
                     const partCodeService = injector.get(PartCodeService);
                     const isValid = await partCodeService.validateCode(value);
                     if (!isValid) {

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

@@ -3,13 +3,14 @@ import { fail } from 'assert';
 import { describe, expect, it } from 'vitest';
 
 import { Injector } from '../../common/injector';
+import { RequestContext } from './request-context';
 
 import { validateCustomFieldValue } from './validate-custom-field-value';
 
 describe('validateCustomFieldValue()', () => {
     const injector = new Injector({} as any);
 
-    async function assertThrowsError(validateFn: () => Promise<void>, message: string) {
+    async function assertThrowsError(validateFn: (() => Promise<void>) | (() => void), message: string) {
         try {
             await validateFn();
             fail('Should have thrown');
@@ -18,6 +19,8 @@ describe('validateCustomFieldValue()', () => {
         }
     }
 
+    const ctx = RequestContext.empty();
+
     describe('string & localeString', () => {
         const validate = (value: string) => () =>
             validateCustomFieldValue(
@@ -28,6 +31,7 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
+                ctx,
             );
 
         it('passes valid pattern', async () => {
@@ -53,6 +57,7 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
+                ctx,
             );
 
         it('passes valid option', async () => {
@@ -78,6 +83,7 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
+                ctx,
             );
 
         it('passes valid range', async () => {
@@ -104,6 +110,7 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
+                ctx,
             );
 
         it('passes valid range', async () => {
@@ -138,9 +145,14 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
+                ctx,
             );
-        const validate2 = (value: string, languageCode: LanguageCode) => () =>
-            validateCustomFieldValue(
+        const validate2 = (value: string, languageCode: LanguageCode) => () => {
+            const ctxWithLanguage = new RequestContext({
+                languageCode,
+                apiType: 'admin',
+            } as any);
+            return validateCustomFieldValue(
                 {
                     name: 'test',
                     type: 'string',
@@ -155,8 +167,9 @@ describe('validateCustomFieldValue()', () => {
                 },
                 value,
                 injector,
-                languageCode,
+                ctxWithLanguage,
             );
+        };
 
         it('passes validate fn string', async () => {
             expect(validate1('valid')).not.toThrow();
@@ -192,6 +205,7 @@ describe('validateCustomFieldValue()', () => {
                     },
                     value,
                     injector,
+                    ctx,
                 );
 
             expect(validate([1, 2, 6])).not.toThrow();
@@ -209,6 +223,7 @@ describe('validateCustomFieldValue()', () => {
                     },
                     value,
                     injector,
+                    ctx,
                 );
 
             expect(validate(['small', 'large'])).not.toThrow();
@@ -230,6 +245,7 @@ describe('validateCustomFieldValue()', () => {
                     },
                     value,
                     injector,
+                    ctx,
                 );
 
             expect(validate(['valid', 'valid'])).not.toThrow();

+ 6 - 5
packages/core/src/api/common/validate-custom-field-value.ts

@@ -12,6 +12,7 @@ import {
     StringCustomFieldConfig,
     TypedCustomFieldConfig,
 } from '../../config/custom-field/custom-field-types';
+import { RequestContext } from './request-context';
 
 /**
  * Validates the value of a custom field input against any configured constraints.
@@ -21,7 +22,7 @@ export async function validateCustomFieldValue(
     config: CustomFieldConfig,
     value: any | any[],
     injector: Injector,
-    languageCode?: LanguageCode,
+    ctx: RequestContext,
 ): Promise<void> {
     if (config.readonly) {
         throw new UserInputError('error.field-invalid-readonly', { name: config.name });
@@ -40,7 +41,7 @@ export async function validateCustomFieldValue(
     } else {
         validateSingleValue(config, value);
     }
-    await validateCustomFunction(config as TypedCustomFieldConfig<any, any>, value, injector, languageCode);
+    await validateCustomFunction(config as TypedCustomFieldConfig<any, any>, value, injector, ctx);
 }
 
 function validateSingleValue(config: CustomFieldConfig, value: any) {
@@ -70,15 +71,15 @@ async function validateCustomFunction<T extends TypedCustomFieldConfig<any, any>
     config: T,
     value: any,
     injector: Injector,
-    languageCode?: LanguageCode,
+    ctx: RequestContext,
 ) {
     if (typeof config.validate === 'function') {
-        const error = await config.validate(value, injector);
+        const error = await config.validate(value, injector, ctx);
         if (typeof error === 'string') {
             throw new UserInputError(error);
         }
         if (Array.isArray(error)) {
-            const localizedError = error.find(e => e.languageCode === languageCode) || error[0];
+            const localizedError = error.find(e => e.languageCode === ctx.languageCode) || error[0];
             throw new UserInputError(localizedError.value);
         }
     }

+ 2 - 2
packages/core/src/api/config/get-custom-fields-config-without-interfaces.ts

@@ -1,6 +1,6 @@
 import { GraphQLSchema, isInterfaceType } from 'graphql';
 
-import { CustomFields, CustomFieldConfig } from '../../config/custom-field/custom-field-types';
+import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
 
 /**
  * @description
@@ -23,7 +23,7 @@ export function getCustomFieldsConfigWithoutInterfaces(
             entries.splice(regionIndex, 1);
 
             for (const implementation of implementations.objects) {
-                entries.push([implementation.name, customFieldConfig.Region]);
+                entries.push([implementation.name, customFieldConfig.Region ?? []]);
             }
         }
     }

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

@@ -59,7 +59,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
                                 : [variables[inputName]];
 
                             for (const inputVariable of inputVariables) {
-                                await this.validateInput(typeName, ctx.languageCode, injector, inputVariable);
+                                await this.validateInput(typeName, ctx, injector, inputVariable);
                             }
                         }
                     }
@@ -71,7 +71,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
 
     private async validateInput(
         typeName: string,
-        languageCode: LanguageCode,
+        ctx: RequestContext,
         injector: Injector,
         variableValues?: { [key: string]: any },
     ) {
@@ -83,7 +83,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
                 // mutations.
                 await this.validateCustomFieldsObject(
                     this.configService.customFields.OrderLine,
-                    languageCode,
+                    ctx,
                     variableValues,
                     injector,
                 );
@@ -91,7 +91,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
             if (variableValues.customFields) {
                 await this.validateCustomFieldsObject(
                     customFieldConfig,
-                    languageCode,
+                    ctx,
                     variableValues.customFields,
                     injector,
                 );
@@ -102,7 +102,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
                     if (translation.customFields) {
                         await this.validateCustomFieldsObject(
                             customFieldConfig,
-                            languageCode,
+                            ctx,
                             translation.customFields,
                             injector,
                         );
@@ -114,14 +114,14 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
 
     private async validateCustomFieldsObject(
         customFieldConfig: CustomFieldConfig[],
-        languageCode: LanguageCode,
+        ctx: RequestContext,
         customFieldsObject: { [key: string]: any },
         injector: Injector,
     ) {
         for (const [key, value] of Object.entries(customFieldsObject)) {
             const config = customFieldConfig.find(c => getGraphQlInputName(c) === key);
             if (config) {
-                await validateCustomFieldValue(config, value, injector, languageCode);
+                await validateCustomFieldValue(config, value, injector, ctx);
             }
         }
     }

+ 3 - 1
packages/core/src/config/custom-field/custom-field-types.ts

@@ -19,6 +19,7 @@ import {
     UiComponentConfig,
 } from '@vendure/common/lib/shared-types';
 
+import { RequestContext } from '../../api/index';
 import { Injector } from '../../common/injector';
 import { VendureEntity } from '../../entity/base/base.entity';
 
@@ -61,6 +62,7 @@ export type TypedCustomSingleFieldConfig<
     validate?: (
         value: DefaultValueType<T>,
         injector: Injector,
+        ctx: RequestContext,
     ) => string | LocalizedString[] | void | Promise<string | LocalizedString[] | void>;
 };
 
@@ -172,7 +174,7 @@ export type CustomFields = {
     TaxRate?: CustomFieldConfig[];
     User?: CustomFieldConfig[];
     Zone?: CustomFieldConfig[];
-} & { [entity: string]: CustomFieldConfig[] | undefined };
+} & { [entity: string]: CustomFieldConfig[] };
 
 /**
  * This interface should be implemented by any entity which can be extended