Skip to content

Commit

Permalink
feat(core): Pass RequestContext to custom field validate function
Browse files Browse the repository at this point in the history
Closes #2408
  • Loading branch information
michaelbromley committed Jan 25, 2024
1 parent 5c5c375 commit 2314ff6
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 21 deletions.
4 changes: 2 additions & 2 deletions docs/docs/guides/developer-guide/custom-fields/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/api/common/validate-custom-field-value.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -18,6 +19,8 @@ describe('validateCustomFieldValue()', () => {
}
}

const ctx = RequestContext.empty();

describe('string & localeString', () => {
const validate = (value: string) => () =>
validateCustomFieldValue(
Expand All @@ -28,6 +31,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

it('passes valid pattern', async () => {
Expand All @@ -53,6 +57,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

it('passes valid option', async () => {
Expand All @@ -78,6 +83,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

it('passes valid range', async () => {
Expand All @@ -104,6 +110,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

it('passes valid range', async () => {
Expand Down Expand Up @@ -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',
Expand All @@ -155,8 +167,9 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
languageCode,
ctxWithLanguage,
);
};

it('passes validate fn string', async () => {
expect(validate1('valid')).not.toThrow();
Expand Down Expand Up @@ -192,6 +205,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

expect(validate([1, 2, 6])).not.toThrow();
Expand All @@ -209,6 +223,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

expect(validate(['small', 'large'])).not.toThrow();
Expand All @@ -230,6 +245,7 @@ describe('validateCustomFieldValue()', () => {
},
value,
injector,
ctx,
);

expect(validate(['valid', 'valid'])).not.toThrow();
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/api/common/validate-custom-field-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 });
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ?? []]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -71,7 +71,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {

private async validateInput(
typeName: string,
languageCode: LanguageCode,
ctx: RequestContext,
injector: Injector,
variableValues?: { [key: string]: any },
) {
Expand All @@ -83,15 +83,15 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
// mutations.
await this.validateCustomFieldsObject(
this.configService.customFields.OrderLine,
languageCode,
ctx,
variableValues,
injector,
);
}
if (variableValues.customFields) {
await this.validateCustomFieldsObject(
customFieldConfig,
languageCode,
ctx,
variableValues.customFields,
injector,
);
Expand All @@ -102,7 +102,7 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
if (translation.customFields) {
await this.validateCustomFieldsObject(
customFieldConfig,
languageCode,
ctx,
translation.customFields,
injector,
);
Expand All @@ -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);
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/config/custom-field/custom-field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -61,6 +62,7 @@ export type TypedCustomSingleFieldConfig<
validate?: (
value: DefaultValueType<T>,
injector: Injector,
ctx: RequestContext,
) => string | LocalizedString[] | void | Promise<string | LocalizedString[] | void>;
};

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2314ff6

Please sign in to comment.