Przeglądaj źródła

fix(core): Apply custom field defaults on entity creation (#3674)

Michael Bromley 6 miesięcy temu
rodzic
commit
65804ba7be

+ 278 - 0
packages/core/e2e/custom-field-default-values.e2e-spec.ts

@@ -0,0 +1,278 @@
+import { createTestEnvironment } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+
+/**
+ * Tests for GitHub issue #3266: Custom field default values not applied when explicitly set to null
+ * https://github.com/vendure-ecommerce/vendure/issues/3266
+ */
+
+const customConfig = {
+    ...testConfig(),
+    customFields: {
+        Product: [
+            { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
+            { name: 'intWithDefault', type: 'int', defaultValue: 5 },
+            { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
+        ],
+        Customer: [
+            { name: 'stringWithDefault', type: 'string', defaultValue: 'customer-default' },
+            { name: 'intWithDefault', type: 'int', defaultValue: 100 },
+            { name: 'booleanWithDefault', type: 'boolean', defaultValue: false },
+        ],
+    },
+};
+
+describe('Custom field default values', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
+
+    const CREATE_CUSTOMER = gql`
+        mutation CreateCustomer($input: CreateCustomerInput!) {
+            createCustomer(input: $input) {
+                ... on Customer {
+                    id
+                    firstName
+                    lastName
+                    emailAddress
+                    customFields {
+                        stringWithDefault
+                        intWithDefault
+                        booleanWithDefault
+                    }
+                }
+                ... on ErrorResult {
+                    errorCode
+                    message
+                }
+            }
+        }
+    `;
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('translatable entity (Product)', () => {
+        it('should apply default values when creating product without custom fields', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product 1"
+                                    slug: "test-product-1"
+                                    description: "Test"
+                                }
+                            ]
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should apply default values when creating product with empty custom fields', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product 2"
+                                    slug: "test-product-2"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {}
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should apply default values when custom fields are explicitly set to null', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product Null"
+                                    slug: "test-product-null"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {
+                                stringWithDefault: null
+                                intWithDefault: null
+                                booleanWithDefault: null
+                            }
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            // This is the core issue: when custom fields are explicitly set to null,
+            // they should still get their default values
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should not override explicitly provided values', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product Custom"
+                                    slug: "test-product-custom"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {
+                                stringWithDefault: "custom value"
+                                intWithDefault: 999
+                                booleanWithDefault: false
+                            }
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            // When explicit values are provided, they should be used instead of defaults
+            expect(createProduct.customFields.stringWithDefault).toBe('custom value');
+            expect(createProduct.customFields.intWithDefault).toBe(999);
+            expect(createProduct.customFields.booleanWithDefault).toBe(false);
+        });
+    });
+
+    describe('non-translatable entity (Customer)', () => {
+        it('should apply default values when creating customer without custom fields', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'John',
+                    lastName: 'Doe',
+                    emailAddress: 'john.doe@example.com',
+                },
+            });
+
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should apply default values when creating customer with empty custom fields', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Jane',
+                    lastName: 'Smith',
+                    emailAddress: 'jane.smith@example.com',
+                    customFields: {},
+                },
+            });
+
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should apply default values when custom fields are explicitly set to null', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Bob',
+                    lastName: 'Johnson',
+                    emailAddress: 'bob.johnson@example.com',
+                    customFields: {
+                        stringWithDefault: null,
+                        intWithDefault: null,
+                        booleanWithDefault: null,
+                    },
+                },
+            });
+
+            // This should reproduce the issue for non-translatable entities
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should not override explicitly provided values', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Alice',
+                    lastName: 'Wilson',
+                    emailAddress: 'alice.wilson@example.com',
+                    customFields: {
+                        stringWithDefault: 'custom customer value',
+                        intWithDefault: 777,
+                        booleanWithDefault: true,
+                    },
+                },
+            });
+
+            // When explicit values are provided, they should be used instead of defaults
+            expect(createCustomer.customFields.stringWithDefault).toBe('custom customer value');
+            expect(createCustomer.customFields.intWithDefault).toBe(777);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(true);
+        });
+    });
+});

+ 4 - 5
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -4,11 +4,10 @@ import { createTestEnvironment } from '@vendure/testing';
 import { fail } from 'assert';
 import gql from 'graphql-tag';
 import path from 'path';
-import { vi } from 'vitest';
-import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { fixPostgresTimezone } from './utils/fix-pg-timezone';
@@ -192,8 +191,8 @@ const customConfig = mergeConfig(testConfig(), {
             {
                 name: 'costPrice',
                 type: 'int',
-            }
-        ],  
+            },
+        ],
         // Single readonly Address custom field to test
         // https://github.com/vendure-ecommerce/vendure/issues/3326
         Address: [

+ 3 - 2
packages/core/src/api/api.module.ts

@@ -11,10 +11,10 @@ import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-m
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { VENDURE_ADMIN_API_TYPE_PATHS, VENDURE_SHOP_API_TYPE_PATHS } from './constants';
 import { AuthGuard } from './middleware/auth-guard';
+import { CustomFieldProcessingInterceptor } from './middleware/custom-field-processing-interceptor';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { IdInterceptor } from './middleware/id-interceptor';
 import { TranslateErrorResultInterceptor } from './middleware/translate-error-result-interceptor';
-import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fields-interceptor';
 
 /**
  * The ApiModule is responsible for the public API of the application. This is where requests
@@ -60,7 +60,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         },
         {
             provide: APP_INTERCEPTOR,
-            useClass: ValidateCustomFieldsInterceptor,
+            useClass: CustomFieldProcessingInterceptor,
         },
         {
             provide: APP_INTERCEPTOR,
@@ -74,6 +74,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
 })
 export class ApiModule implements NestModule {
     constructor(private configService: ConfigService) {}
+
     async configure(consumer: MiddlewareConsumer) {
         const { adminApiPath, shopApiPath } = this.configService.apiOptions;
         const { uploadMaxFileSize } = this.configService.assetOptions;

+ 288 - 0
packages/core/src/api/middleware/custom-field-processing-interceptor.ts

@@ -0,0 +1,288 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { getGraphQlInputName } from '@vendure/common/lib/shared-utils';
+import {
+    getNamedType,
+    GraphQLSchema,
+    OperationDefinitionNode,
+    TypeInfo,
+    visit,
+    visitWithTypeInfo,
+} from 'graphql';
+
+import { Injector } from '../../common/injector';
+import { ConfigService } from '../../config/config.service';
+import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
+import { parseContext } from '../common/parse-context';
+import { internal_getRequestContext, RequestContext } from '../common/request-context';
+import { validateCustomFieldValue } from '../common/validate-custom-field-value';
+
+/**
+ * @description
+ * Unified interceptor that processes custom fields in GraphQL mutations by:
+ *
+ * 1. Applying default values when fields are explicitly set to null (create operations only)
+ * 2. Validating custom field values according to their constraints
+ *
+ * Uses native GraphQL utilities (visit, visitWithTypeInfo, getNamedType) for efficient
+ * AST traversal and type analysis.
+ */
+@Injectable()
+export class CustomFieldProcessingInterceptor implements NestInterceptor {
+    private readonly createInputsWithCustomFields = new Set<string>();
+    private readonly updateInputsWithCustomFields = new Set<string>();
+
+    constructor(
+        private readonly configService: ConfigService,
+        private readonly moduleRef: ModuleRef,
+    ) {
+        Object.keys(configService.customFields).forEach(entityName => {
+            this.createInputsWithCustomFields.add(`Create${entityName}Input`);
+            this.updateInputsWithCustomFields.add(`Update${entityName}Input`);
+        });
+        // Note: OrderLineCustomFieldsInput is handled separately since it's used in both
+        // create operations (addItemToOrder) and update operations (adjustOrderLine)
+    }
+
+    async intercept(context: ExecutionContext, next: CallHandler<any>) {
+        const parsedContext = parseContext(context);
+
+        if (!parsedContext.isGraphQL) {
+            return next.handle();
+        }
+
+        const { operation, schema } = parsedContext.info;
+        if (operation.operation === 'mutation') {
+            await this.processMutationCustomFields(context, operation, schema);
+        }
+
+        return next.handle();
+    }
+
+    private async processMutationCustomFields(
+        context: ExecutionContext,
+        operation: OperationDefinitionNode,
+        schema: GraphQLSchema,
+    ) {
+        const gqlExecutionContext = GqlExecutionContext.create(context);
+        const variables = gqlExecutionContext.getArgs();
+        const ctx = internal_getRequestContext(parseContext(context).req);
+        const injector = new Injector(this.moduleRef);
+
+        const inputTypeNames = this.getArgumentMap(operation, schema);
+
+        for (const [inputName, typeName] of Object.entries(inputTypeNames)) {
+            if (this.hasCustomFields(typeName) && variables[inputName]) {
+                await this.processInputVariables(typeName, variables[inputName], ctx, injector, operation);
+            }
+        }
+    }
+
+    private hasCustomFields(typeName: string): boolean {
+        return (
+            this.createInputsWithCustomFields.has(typeName) ||
+            this.updateInputsWithCustomFields.has(typeName) ||
+            typeName === 'OrderLineCustomFieldsInput'
+        );
+    }
+
+    private async processInputVariables(
+        typeName: string,
+        variableInput: any,
+        ctx: RequestContext,
+        injector: Injector,
+        operation: OperationDefinitionNode,
+    ) {
+        const inputVariables = Array.isArray(variableInput) ? variableInput : [variableInput];
+        const shouldApplyDefaults = this.shouldApplyDefaults(typeName, operation);
+
+        for (const inputVariable of inputVariables) {
+            if (shouldApplyDefaults) {
+                this.applyDefaultsToInput(typeName, inputVariable);
+            }
+            await this.validateInput(typeName, ctx, injector, inputVariable);
+        }
+    }
+
+    private shouldApplyDefaults(typeName: string, operation: OperationDefinitionNode): boolean {
+        // For regular create inputs, always apply defaults
+        if (this.createInputsWithCustomFields.has(typeName)) {
+            return true;
+        }
+
+        // For OrderLineCustomFieldsInput, check the actual mutation name
+        if (typeName === 'OrderLineCustomFieldsInput') {
+            return this.isOrderLineCreateOperation(operation);
+        }
+
+        // For update inputs, never apply defaults
+        return false;
+    }
+
+    private isOrderLineCreateOperation(operation: OperationDefinitionNode): boolean {
+        // Check if any field in the operation is a "create/add" operation for order lines
+        for (const selection of operation.selectionSet.selections) {
+            if (selection.kind === 'Field') {
+                const fieldName = selection.name.value;
+                // These mutations create new order lines, so should apply defaults
+                if (fieldName === 'addItemToOrder' || fieldName === 'addItemToDraftOrder') {
+                    return true;
+                }
+                // These mutations modify existing order lines, so should NOT apply defaults
+                if (fieldName === 'adjustOrderLine' || fieldName === 'adjustDraftOrderLine') {
+                    return false;
+                }
+            }
+        }
+        // Default to false for safety (don't apply defaults unless we're sure it's a create)
+        return false;
+    }
+
+    private getArgumentMap(
+        operation: OperationDefinitionNode,
+        schema: GraphQLSchema,
+    ): { [inputName: string]: string } {
+        const typeInfo = new TypeInfo(schema);
+        const map: { [inputName: string]: string } = {};
+
+        const visitor = {
+            enter(node: any) {
+                if (node.kind === 'Field') {
+                    const fieldDef = typeInfo.getFieldDef();
+                    if (fieldDef) {
+                        for (const arg of fieldDef.args) {
+                            map[arg.name] = getNamedType(arg.type).name;
+                        }
+                    }
+                }
+            },
+        };
+
+        visit(operation, visitWithTypeInfo(typeInfo, visitor));
+        return map;
+    }
+
+    private applyDefaultsToInput(typeName: string, variableValues: any) {
+        if (typeName === 'OrderLineCustomFieldsInput') {
+            this.applyDefaultsForOrderLine(variableValues);
+        } else {
+            this.applyDefaultsForEntity(typeName, variableValues);
+        }
+    }
+
+    private applyDefaultsForOrderLine(variableValues: any) {
+        const orderLineConfig = this.configService.customFields.OrderLine || [];
+        this.applyDefaultsToCustomFieldsObject(orderLineConfig, variableValues);
+    }
+
+    private applyDefaultsForEntity(typeName: string, variableValues: any) {
+        const entityName = this.getEntityNameFromInputType(typeName);
+        const customFieldConfig = this.configService.customFields[entityName];
+
+        if (!customFieldConfig) {
+            return;
+        }
+
+        this.applyDefaultsToDirectCustomFields(customFieldConfig, variableValues);
+        this.applyDefaultsToTranslationCustomFields(customFieldConfig, variableValues);
+    }
+
+    private applyDefaultsToDirectCustomFields(customFieldConfig: any[], variableValues: any) {
+        if (variableValues.customFields) {
+            this.applyDefaultsToCustomFieldsObject(customFieldConfig, variableValues.customFields);
+        }
+    }
+
+    private applyDefaultsToTranslationCustomFields(customFieldConfig: any[], variableValues: any) {
+        if (!variableValues.translations || !Array.isArray(variableValues.translations)) {
+            return;
+        }
+
+        for (const translation of variableValues.translations) {
+            if (translation.customFields) {
+                this.applyDefaultsToCustomFieldsObject(customFieldConfig, translation.customFields);
+            }
+        }
+    }
+
+    private applyDefaultsToCustomFieldsObject(customFieldConfig: any[], customFieldsObject: any) {
+        for (const config of customFieldConfig) {
+            const fieldName = getGraphQlInputName(config);
+            // Only apply default if the field is explicitly null and has a default value
+            if (customFieldsObject[fieldName] === null && config.defaultValue !== undefined) {
+                customFieldsObject[fieldName] = config.defaultValue;
+            }
+        }
+    }
+
+    private getEntityNameFromInputType(typeName: string): string {
+        // Remove "Create" or "Update" prefix and "Input" suffix
+        // e.g., "CreateProductInput" -> "Product", "UpdateCustomerInput" -> "Customer"
+        if (typeName.startsWith('Create')) {
+            return typeName.slice(6, -5); // Remove "Create" and "Input"
+        }
+        if (typeName.startsWith('Update')) {
+            return typeName.slice(6, -5); // Remove "Update" and "Input"
+        }
+        return typeName;
+    }
+
+    private async validateInput(
+        typeName: string,
+        ctx: RequestContext,
+        injector: Injector,
+        variableValues?: { [key: string]: any },
+    ) {
+        if (variableValues) {
+            const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
+            const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
+
+            if (typeName === 'OrderLineCustomFieldsInput') {
+                // special case needed to handle custom fields passed via addItemToOrder or adjustOrderLine
+                // mutations.
+                await this.validateCustomFieldsObject(
+                    this.configService.customFields.OrderLine,
+                    ctx,
+                    variableValues,
+                    injector,
+                );
+            }
+            if (variableValues.customFields) {
+                await this.validateCustomFieldsObject(
+                    customFieldConfig,
+                    ctx,
+                    variableValues.customFields,
+                    injector,
+                );
+            }
+            const translations = variableValues.translations;
+            if (Array.isArray(translations)) {
+                for (const translation of translations) {
+                    if (translation.customFields) {
+                        await this.validateCustomFieldsObject(
+                            customFieldConfig,
+                            ctx,
+                            translation.customFields,
+                            injector,
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    private async validateCustomFieldsObject(
+        customFieldConfig: CustomFieldConfig[],
+        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, ctx);
+            }
+        }
+    }
+}

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

@@ -1,172 +0,0 @@
-import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
-import { GqlExecutionContext } from '@nestjs/graphql';
-import { getGraphQlInputName } from '@vendure/common/lib/shared-utils';
-import {
-    GraphQLInputType,
-    GraphQLList,
-    GraphQLNonNull,
-    GraphQLSchema,
-    OperationDefinitionNode,
-    TypeNode,
-} from 'graphql';
-
-import { Injector } from '../../common/injector';
-import { ConfigService } from '../../config/config.service';
-import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
-import { parseContext } from '../common/parse-context';
-import { internal_getRequestContext, RequestContext } from '../common/request-context';
-import { validateCustomFieldValue } from '../common/validate-custom-field-value';
-
-/**
- * This interceptor is responsible for enforcing the validation constraints defined for any CustomFields.
- * For example, if a custom 'int' field has a "min" value of 0, and a mutation attempts to set its value
- * to a negative integer, then that mutation will fail with an error.
- */
-@Injectable()
-export class ValidateCustomFieldsInterceptor implements NestInterceptor {
-    private readonly inputsWithCustomFields: Set<string>;
-
-    constructor(
-        private configService: ConfigService,
-        private moduleRef: ModuleRef,
-    ) {
-        this.inputsWithCustomFields = Object.keys(configService.customFields).reduce((inputs, entityName) => {
-            inputs.add(`Create${entityName}Input`);
-            inputs.add(`Update${entityName}Input`);
-            return inputs;
-        }, new Set<string>());
-        this.inputsWithCustomFields.add('OrderLineCustomFieldsInput');
-    }
-
-    async intercept(context: ExecutionContext, next: CallHandler<any>) {
-        const parsedContext = parseContext(context);
-        const injector = new Injector(this.moduleRef);
-        if (parsedContext.isGraphQL) {
-            const gqlExecutionContext = GqlExecutionContext.create(context);
-            const { operation, schema } = parsedContext.info;
-            const variables = gqlExecutionContext.getArgs();
-            const ctx = internal_getRequestContext(parsedContext.req);
-
-            if (operation.operation === 'mutation') {
-                const inputTypeNames = this.getArgumentMap(operation, schema);
-                for (const [inputName, typeName] of Object.entries(inputTypeNames)) {
-                    if (this.inputsWithCustomFields.has(typeName)) {
-                        if (variables[inputName]) {
-                            const inputVariables: Array<Record<string, any>> = Array.isArray(
-                                variables[inputName],
-                            )
-                                ? variables[inputName]
-                                : [variables[inputName]];
-
-                            for (const inputVariable of inputVariables) {
-                                await this.validateInput(typeName, ctx, injector, inputVariable);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        return next.handle();
-    }
-
-    private async validateInput(
-        typeName: string,
-        ctx: RequestContext,
-        injector: Injector,
-        variableValues?: { [key: string]: any },
-    ) {
-        if (variableValues) {
-            const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
-            const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
-            if (typeName === 'OrderLineCustomFieldsInput') {
-                // special case needed to handle custom fields passed via addItemToOrder or adjustOrderLine
-                // mutations.
-                await this.validateCustomFieldsObject(
-                    this.configService.customFields.OrderLine,
-                    ctx,
-                    variableValues,
-                    injector,
-                );
-            }
-            if (variableValues.customFields) {
-                await this.validateCustomFieldsObject(
-                    customFieldConfig,
-                    ctx,
-                    variableValues.customFields,
-                    injector,
-                );
-            }
-            const translations = variableValues.translations;
-            if (Array.isArray(translations)) {
-                for (const translation of translations) {
-                    if (translation.customFields) {
-                        await this.validateCustomFieldsObject(
-                            customFieldConfig,
-                            ctx,
-                            translation.customFields,
-                            injector,
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    private async validateCustomFieldsObject(
-        customFieldConfig: CustomFieldConfig[],
-        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, ctx);
-            }
-        }
-    }
-
-    private getArgumentMap(
-        operation: OperationDefinitionNode,
-        schema: GraphQLSchema,
-    ): { [inputName: string]: string } {
-        const mutationType = schema.getMutationType();
-        if (!mutationType) {
-            return {};
-        }
-        const map: { [inputName: string]: string } = {};
-
-        for (const selection of operation.selectionSet.selections) {
-            if (selection.kind === 'Field') {
-                const name = selection.name.value;
-
-                const inputType = mutationType.getFields()[name];
-                if (!inputType) continue;
-
-                for (const arg of inputType.args) {
-                    map[arg.name] = this.getInputTypeName(arg.type);
-                }
-            }
-        }
-        return map;
-    }
-
-    private getNamedTypeName(type: TypeNode): string {
-        if (type.kind === 'NonNullType' || type.kind === 'ListType') {
-            return this.getNamedTypeName(type.type);
-        } else {
-            return type.name.value;
-        }
-    }
-
-    private getInputTypeName(type: GraphQLInputType): string {
-        if (type instanceof GraphQLNonNull) {
-            return this.getInputTypeName(type.ofType);
-        }
-        if (type instanceof GraphQLList) {
-            return this.getInputTypeName(type.ofType);
-        }
-        return type.name;
-    }
-}

+ 1 - 2
packages/core/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -1,14 +1,12 @@
 import { Injectable } from '@nestjs/common';
 import { omit } from '@vendure/common/lib/omit';
 import { ID, Type } from '@vendure/common/lib/shared-types';
-import { FindOneOptions } from 'typeorm';
 import { FindManyOptions } from 'typeorm/find-options/FindManyOptions';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { Translatable, TranslatedInput, Translation } from '../../../common/types/locale-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { patchEntity } from '../utils/patch-entity';
 
 import { TranslationDiffer } from './translation-differ';
@@ -67,6 +65,7 @@ export class TranslatableSaver {
         const { ctx, entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
 
         const entity = new entityType(input);
+
         const translations: Array<Translation<T>> = [];
 
         if (input.translations) {

+ 1 - 0
scripts/codegen/generate-graphql-types.ts

@@ -21,6 +21,7 @@ const specFileToIgnore = [
     'custom-field-relations.e2e-spec',
     'custom-field-struct.e2e-spec',
     'custom-field-permissions.e2e-spec',
+    'custom-field-default-values.e2e-spec',
     'order-item-price-calculation-strategy.e2e-spec',
     'list-query-builder.e2e-spec',
     'shop-order.e2e-spec',