From b8ad2b530ff4277164539e24b9cd060adf09213e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Sowi=C5=84ski?= Date: Wed, 11 Feb 2026 10:01:26 +0100 Subject: [PATCH] feat: Enable ecommerce config validation --- frontend/src/lib/api/data-sets.ts | 20 ++++++++++++++++++-- server/catalog/serializers.py | 12 ++++++++++++ server/sync/base.py | 12 +++++++----- server/sync/document/registry.py | 9 ++++----- server/sync/ecommerce/registry.py | 20 +++++++++----------- server/sync/product/registry.py | 10 +++++----- server/sync/utils.py | 6 +++--- 7 files changed, 58 insertions(+), 31 deletions(-) diff --git a/frontend/src/lib/api/data-sets.ts b/frontend/src/lib/api/data-sets.ts index 79e6eb4f..e6daa146 100644 --- a/frontend/src/lib/api/data-sets.ts +++ b/frontend/src/lib/api/data-sets.ts @@ -361,7 +361,7 @@ export class DataSetsApiClient extends BaseApiClient { } async addDataSetECommerceIntegration(dataSetId: number, pluginName: string, config: object): Promise { - await fetch( + const response = await fetch( `${this.apiBase}/api/data_sets/${dataSetId}/ecommerce_integration`, { ...this._requestConfiguration(), @@ -369,6 +369,14 @@ export class DataSetsApiClient extends BaseApiClient { body: JSON.stringify({ plugin_name: pluginName, config: config }) } ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new ApiError(`Failed to add e-commerce integration: ${response.statusText}`, { + data: errorData, + status: response.status + }); + } } async configureDataSetECommerceIntegration(updated_source: CatalogSource): Promise { @@ -379,13 +387,21 @@ export class DataSetsApiClient extends BaseApiClient { data_set_id: updated_source.data_set_id }; - await fetch(`${this.apiBase}/api/data_sets/${updated_source.data_set_id}/ecommerce_integration`, + const response = await fetch(`${this.apiBase}/api/data_sets/${updated_source.data_set_id}/ecommerce_integration`, { ...this._requestConfiguration(), body: JSON.stringify(body), method: 'PATCH' } ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new ApiError(`Failed to configure e-commerce integration: ${response.statusText}`, { + data: errorData, + status: response.status + }); + } } async syncDataSetEcommerceIntegration(dataSetId: number): Promise { diff --git a/server/catalog/serializers.py b/server/catalog/serializers.py index 89b53fca..05a5c35c 100644 --- a/server/catalog/serializers.py +++ b/server/catalog/serializers.py @@ -2,6 +2,7 @@ from utils.serializers import ParentDataContextSerializerMixin from sync.document.registry import DocumentSourcePluginRegistry +from sync.ecommerce.registry import ECommerceIntegrationPluginRegistry from sync.product.registry import ProductSourcePluginRegistry from .models import DataSet, Document, DocumentSource, ECommerceIntegration, Product, ProductSource @@ -21,6 +22,7 @@ class Meta: "embedding_vector_dimensions", ] + class DataSetCreateSerializer(DataSetSerializer): preconfigure_agents = serializers.BooleanField(write_only=True, required=False, default=False) @@ -63,6 +65,15 @@ class DocumentSourceConfigSerializer(serializers.Serializer): ) +class ECommerceIntegrationConfigSerializer(serializers.Serializer): + configuration_args = PydanticModelField( + config_field_name="CONFIGURATION_ARGS", + plugin_registry_class=ECommerceIntegrationPluginRegistry, + allow_null=True, + default=None, + ) + + class ProductSourceSerializer(ParentDataContextSerializerMixin, serializers.ModelSerializer): context_keys_to_propagate = ["plugin_name"] @@ -88,6 +99,7 @@ class Meta: class ECommerceIntegrationSerializer(ParentDataContextSerializerMixin, serializers.ModelSerializer): context_keys_to_propagate = ["plugin_name"] + config = ECommerceIntegrationConfigSerializer() task_id = serializers.CharField(read_only=True, required=False, allow_null=True) class Meta: diff --git a/server/sync/base.py b/server/sync/base.py index 0d610aa3..361b5117 100644 --- a/server/sync/base.py +++ b/server/sync/base.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Generic, Self, Type, TypeVar +from typing import Generic, Type, TypeVar + +from utils.base_registry import BaseRegistry @dataclass @@ -15,7 +17,7 @@ class DataSetSource: T = TypeVar("T") -class SourcePluginRegistry(ABC): +class SourcePluginRegistry(Generic[T], BaseRegistry[T], ABC): """Registry of available source plugins registered in ECL system.""" def __init__(self): @@ -23,7 +25,7 @@ def __init__(self): def load_plugins(self): plugins = {} - for plugin_name, _ in self.get_plugins(): + for plugin_name in self.get_plugin_names(): plugins[plugin_name] = self.get_plugin_class_by_name(plugin_name) return plugins @@ -42,11 +44,11 @@ def get_plugin_instance(self, data_set_source: DataSetSource): return plugin_instance @abstractmethod - def get_plugins(self) -> dict: + def get_plugin_names(self) -> list[str]: pass @abstractmethod - def get_plugin_class_by_name(self, path: str) -> Type[Self]: + def get_plugin_class_by_name(self, path: str) -> Type[T]: pass diff --git a/server/sync/document/registry.py b/server/sync/document/registry.py index cb321884..a04c246c 100644 --- a/server/sync/document/registry.py +++ b/server/sync/document/registry.py @@ -2,19 +2,18 @@ from django.conf import settings from enthusiast_common import DocumentSourcePlugin -from utils.base_registry import BaseRegistry from sync.base import SourcePluginRegistry -class DocumentSourcePluginRegistry(SourcePluginRegistry, BaseRegistry[DocumentSourcePlugin]): +class DocumentSourcePluginRegistry(SourcePluginRegistry[DocumentSourcePlugin]): """Registry of document source plugins.""" plugin_base = DocumentSourcePlugin - def get_plugins(self): - return settings.CATALOG_DOCUMENT_SOURCE_PLUGINS.items() + def get_plugin_names(self) -> list[str]: + return settings.CATALOG_DOCUMENT_SOURCE_PLUGINS.keys() def get_plugin_class_by_name(self, name: str) -> Type[DocumentSourcePlugin]: path = settings.CATALOG_DOCUMENT_SOURCE_PLUGINS[name] - return self._get_plugin_class_by_path(path) \ No newline at end of file + return self._get_plugin_class_by_path(path) diff --git a/server/sync/ecommerce/registry.py b/server/sync/ecommerce/registry.py index 0bfec43c..de1c6c43 100644 --- a/server/sync/ecommerce/registry.py +++ b/server/sync/ecommerce/registry.py @@ -2,23 +2,15 @@ from django.conf import settings from enthusiast_common.interfaces import ECommerceIntegrationPlugin -from utils.base_registry import BaseRegistry -from catalog.models import ECommerceIntegration +from sync.base import SourcePluginRegistry -class ECommerceIntegrationPluginRegistry(BaseRegistry[ECommerceIntegrationPlugin]): +class ECommerceIntegrationPluginRegistry(SourcePluginRegistry[ECommerceIntegrationPlugin]): """Registry of ecommerce integration plugins.""" plugin_base = ECommerceIntegrationPlugin - def get_plugin_instance(self, ecommerce_integration: ECommerceIntegration) -> ECommerceIntegrationPlugin: - plugin_classes_by_names = self._get_plugin_classes_by_names() - plugin_class = plugin_classes_by_names[ecommerce_integration.plugin_name] - plugin_instance = plugin_class(data_set_id=ecommerce_integration.data_set_id) - plugin_instance.set_runtime_arguments(ecommerce_integration.config) - return plugin_instance - def get_plugin_classes(self) -> List[Type[ECommerceIntegrationPlugin]]: return [self._get_plugin_class_by_path(path) for path in self._get_plugin_paths()] @@ -27,4 +19,10 @@ def _get_plugin_paths() -> List[str]: return settings.CATALOG_ECOMMERCE_INTEGRATION_PLUGINS def _get_plugin_classes_by_names(self) -> dict[str, Type[ECommerceIntegrationPlugin]]: - return { plugin_class.NAME: plugin_class for plugin_class in self.get_plugin_classes() } + return {plugin_class.NAME: plugin_class for plugin_class in self.get_plugin_classes()} + + def get_plugin_class_by_name(self, name: str) -> Type[ECommerceIntegrationPlugin]: + return self._get_plugin_classes_by_names()[name] + + def get_plugin_names(self) -> list[str]: + return list(self._get_plugin_classes_by_names().keys()) diff --git a/server/sync/product/registry.py b/server/sync/product/registry.py index 9e6e312d..3d7ec140 100644 --- a/server/sync/product/registry.py +++ b/server/sync/product/registry.py @@ -2,18 +2,18 @@ from django.conf import settings from enthusiast_common import ProductSourcePlugin -from utils.base_registry import BaseRegistry from sync.base import SourcePluginRegistry -class ProductSourcePluginRegistry(SourcePluginRegistry, BaseRegistry[ProductSourcePlugin]): +class ProductSourcePluginRegistry(SourcePluginRegistry[ProductSourcePlugin]): """Registry of product sync plugins.""" + plugin_base = ProductSourcePlugin - def get_plugins(self): - return settings.CATALOG_PRODUCT_SOURCE_PLUGINS.items() + def get_plugin_names(self) -> list[str]: + return settings.CATALOG_PRODUCT_SOURCE_PLUGINS.keys() def get_plugin_class_by_name(self, name: str) -> Type[ProductSourcePlugin]: path = settings.CATALOG_PRODUCT_SOURCE_PLUGINS[name] - return self._get_plugin_class_by_path(path) \ No newline at end of file + return self._get_plugin_class_by_path(path) diff --git a/server/sync/utils.py b/server/sync/utils.py index 18ee73fb..d133683d 100644 --- a/server/sync/utils.py +++ b/server/sync/utils.py @@ -9,12 +9,12 @@ class PluginTypesMixin: def get_choices(self, plugin_registry_class: Type[SourcePluginRegistry]): plugin_registry = plugin_registry_class() choices = [] - for key, _ in plugin_registry.get_plugins(): + for name in plugin_registry.get_plugin_names(): choices.append( { - "name": key, + "name": name, "configuration_args": get_model_descriptor_from_class_field( - plugin_registry.get_plugin_class_by_name(key), "CONFIGURATION_ARGS" + plugin_registry.get_plugin_class_by_name(name), "CONFIGURATION_ARGS" ), } )