Переглянути джерело

fix(core): Log warning when attempting to persist invalid custom fields (#3793)

Bibiana Sebestianova 4 місяців тому
батько
коміт
eefbd9cac8

+ 134 - 1
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -1,5 +1,14 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { Asset, CustomFields, mergeConfig, TransactionalConnection } from '@vendure/core';
+import {
+    Asset,
+    CustomFields,
+    Logger,
+    mergeConfig,
+    OrderService,
+    ProductService,
+    RequestContextService,
+    TransactionalConnection,
+} from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import { fail } from 'assert';
 import gql from 'graphql-tag';
@@ -1215,4 +1224,128 @@ describe('Custom fields', () => {
             },
         ]);
     });
+
+    describe('setting custom fields directly via a service method', () => {
+        it('OrderService.addItemToOrder warns on unknown custom field', async () => {
+            const orderService = server.app.get(OrderService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+
+            const order = await orderService.create(ctx);
+
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await orderService.addItemToOrder(ctx, order.id, 1, 1, {
+                customFieldWhichDoesNotExist: 'test value',
+            });
+
+            expect(warnSpy).toHaveBeenCalledWith(
+                'Custom field customFieldWhichDoesNotExist not found for entity OrderLine',
+            );
+        });
+
+        it('OrderService.addItemToOrder does not warn on known custom field', async () => {
+            const orderService = server.app.get(OrderService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+
+            const order = await orderService.create(ctx);
+
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await orderService.addItemToOrder(ctx, order.id, 1, 1, {
+                validateInt: 1,
+            });
+
+            expect(warnSpy).not.toHaveBeenCalled();
+        });
+
+        it('OrderService.addItemToOrder warns on multiple unknown custom fields', async () => {
+            const orderService = server.app.get(OrderService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+
+            const order = await orderService.create(ctx);
+
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await orderService.addItemToOrder(ctx, order.id, 1, 1, {
+                unknownField1: 'foo',
+                unknownField2: 'bar',
+            });
+
+            expect(warnSpy).toHaveBeenCalledWith('Custom field unknownField1 not found for entity OrderLine');
+            expect(warnSpy).toHaveBeenCalledWith('Custom field unknownField2 not found for entity OrderLine');
+        });
+
+        it('OrderService.addItemToOrder does not warn when no custom fields are provided', async () => {
+            const orderService = server.app.get(OrderService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+
+            const order = await orderService.create(ctx);
+
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await orderService.addItemToOrder(ctx, order.id, 1, 1);
+
+            expect(warnSpy).not.toHaveBeenCalled();
+        });
+
+        it('warns on unknown custom field in ProductTranslation entity', async () => {
+            const productService = server.app.get(ProductService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await productService.create(ctx, {
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'test',
+                        slug: 'test',
+                        description: '',
+                        customFields: { customFieldWhichDoesNotExist: 'foo' },
+                    },
+                ],
+            });
+
+            expect(warnSpy).toHaveBeenCalledWith(
+                'Custom field customFieldWhichDoesNotExist not found for entity ProductTranslation',
+            );
+        });
+
+        it('does not warn when Translation has a valid custom field', async () => {
+            const productService = server.app.get(ProductService);
+            const requestContextService = server.app.get(RequestContextService);
+            const ctx = await requestContextService.create({
+                apiType: 'admin',
+            });
+            const warnSpy = vi.spyOn(Logger, 'warn');
+
+            await productService.create(ctx, {
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'test',
+                        slug: 'test',
+                        description: '',
+                        customFields: { localeStringWithDefault: 'foo' },
+                    },
+                ],
+            });
+
+            expect(warnSpy).not.toHaveBeenCalled();
+        });
+    });
 });

+ 19 - 3
packages/core/src/connection/connection.module.ts

@@ -6,16 +6,32 @@ import { ConfigModule } from '../config/config.module';
 import { ConfigService } from '../config/config.service';
 import { TypeOrmLogger } from '../config/logger/typeorm-logger';
 
+import { CustomFieldsValidationSubscriber } from './custom-fields-validation-subscriber';
 import { TransactionSubscriber } from './transaction-subscriber';
 import { TransactionWrapper } from './transaction-wrapper';
 import { TransactionalConnection } from './transactional-connection';
 
 let defaultTypeOrmModule: DynamicModule;
-
 @Module({
     imports: [ConfigModule],
-    providers: [TransactionalConnection, TransactionSubscriber, TransactionWrapper],
-    exports: [TransactionalConnection, TransactionSubscriber, TransactionWrapper],
+    providers: [
+        TransactionalConnection,
+        TransactionSubscriber,
+        TransactionWrapper,
+        CustomFieldsValidationSubscriber,
+    ],
+    exports: [
+        TransactionalConnection,
+        TransactionSubscriber,
+        TransactionWrapper,
+        CustomFieldsValidationSubscriber,
+    ],
+})
+export class ConnectionCoreModule {}
+
+@Module({
+    imports: [ConnectionCoreModule],
+    exports: [ConnectionCoreModule],
 })
 export class ConnectionModule {
     static forRoot(): DynamicModule {

+ 63 - 0
packages/core/src/connection/custom-fields-validation-subscriber.ts

@@ -0,0 +1,63 @@
+import { Injectable } from '@nestjs/common';
+import { EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm';
+
+import { ConfigService } from '../config/config.service';
+import { CustomFields, HasCustomFields } from '../config/custom-field/custom-field-types';
+import { Logger } from '../config/logger/vendure-logger';
+
+import { TransactionalConnection } from './transactional-connection';
+
+@Injectable()
+export class CustomFieldsValidationSubscriber implements EntitySubscriberInterface {
+    constructor(
+        private connection: TransactionalConnection,
+        private configService: ConfigService,
+    ) {
+        connection.rawConnection.subscribers.push(this);
+    }
+
+    validateCustomFields(entityName: string, entity: Partial<HasCustomFields>) {
+        const cf: any = (entity as any).customFields;
+        if (cf === null || cf === undefined || typeof cf !== 'object') {
+            return;
+        }
+
+        const config = this.resolveCustomFieldsConfig(entityName);
+        if (!config || config.length === 0) {
+            return;
+        }
+        const validFieldNames = new Set(config.map(field => field.name));
+        for (const key of Object.keys(cf)) {
+            if (!validFieldNames.has(key)) {
+                Logger.warn(`Custom field ${key} not found for entity ${entityName}`);
+            }
+        }
+    }
+
+    private resolveCustomFieldsConfig(entityName: string) {
+        let cfg = this.configService.customFields[entityName as keyof CustomFields];
+        if (!cfg && entityName.endsWith('Translation')) {
+            const base = entityName.slice(0, -'Translation'.length);
+            cfg = this.configService.customFields[base as keyof CustomFields];
+        }
+        return cfg;
+    }
+
+    beforeInsert(event: InsertEvent<any>) {
+        if (event.entity === undefined) {
+            return;
+        }
+
+        const entityName = event.entity.constructor.name;
+        this.validateCustomFields(entityName, event.entity);
+    }
+
+    beforeUpdate(event: UpdateEvent<any>) {
+        if (event.entity === undefined) {
+            return;
+        }
+
+        const entityName = event.entity.constructor.name;
+        this.validateCustomFields(entityName, event.entity);
+    }
+}