From 80efc7075f04c8fa77aaae24a3b10d778ce3ef5f Mon Sep 17 00:00:00 2001 From: Onkar Ravgan Date: Thu, 4 Jul 2024 10:57:46 +0530 Subject: [PATCH] Fix #15163: Added SAP ERP Connector --- .../metadata/examples/workflows/saperp.yaml | 24 ++ .../source/database/column_type_parser.py | 16 + .../source/database/saperp/__init__.py | 0 .../source/database/saperp/client.py | 166 ++++++++ .../source/database/saperp/connection.py | 50 +++ .../source/database/saperp/constants.py | 25 ++ .../source/database/saperp/metadata.py | 297 ++++++++++++++ .../source/database/saperp/models.py | 98 +++++ .../resources/datasets/saperp/columns.json | 191 +++++++++ .../resources/datasets/saperp/tables.json | 26 ++ .../unit/topology/database/test_saperp.py | 377 ++++++++++++++++++ .../data/testConnections/database/saperp.json | 20 + .../database/sapErpConnection.json | 66 +++ .../entity/services/databaseService.json | 9 +- .../public/locales/en-US/Database/SapErp.md | 59 +++ .../src/assets/img/service-icon-sap-erp.png | Bin 0 -> 28215 bytes .../common/OwnerLabel/owner-label.less | 2 +- .../ui/src/constants/Services.constant.ts | 2 + .../AddObservabilityPage.tsx | 2 +- .../ui/src/utils/DatabaseServiceUtils.ts | 6 + .../ui/src/utils/ServiceUtilClassBase.ts | 4 + 21 files changed, 1437 insertions(+), 3 deletions(-) create mode 100644 ingestion/src/metadata/examples/workflows/saperp.yaml create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/__init__.py create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/client.py create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/connection.py create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/constants.py create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/metadata.py create mode 100644 ingestion/src/metadata/ingestion/source/database/saperp/models.py create mode 100644 ingestion/tests/unit/resources/datasets/saperp/columns.json create mode 100644 ingestion/tests/unit/resources/datasets/saperp/tables.json create mode 100644 ingestion/tests/unit/topology/database/test_saperp.py create mode 100644 openmetadata-service/src/main/resources/json/data/testConnections/database/saperp.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/entity/services/connections/database/sapErpConnection.json create mode 100644 openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/SapErp.md create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-sap-erp.png diff --git a/ingestion/src/metadata/examples/workflows/saperp.yaml b/ingestion/src/metadata/examples/workflows/saperp.yaml new file mode 100644 index 00000000000..f24f95954b8 --- /dev/null +++ b/ingestion/src/metadata/examples/workflows/saperp.yaml @@ -0,0 +1,24 @@ +source: + type: SapErp + serviceName: local_saperp + serviceConnection: + config: + type: SapErp + hostPort: https://localhost.com + apiKey: api_key + databaseName: databaseName + databaseSchema: databaseSchema + paginationLimit: 10 + sourceConfig: + config: + type: DatabaseMetadata +sink: + type: metadata-rest + config: {} +workflowConfig: +# loggerLevel: INFO # DEBUG, INFO, WARN or ERROR + openMetadataServerConfig: + hostPort: http://localhost:8585/api + authProvider: openmetadata + securityConfig: + jwtToken: "eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzQm90IjpmYWxzZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJpYXQiOjE2NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.tS8um_5DKu7HgzGBzS1VTA5uUjKWOCU0B_j08WXBiEC0mr0zNREkqVfwFDD-d24HlNEbrqioLsBuFRiwIWKc1m_ZlVQbG7P36RUxhuv2vbSp80FKyNM-Tj93FDzq91jsyNmsQhyNv_fNr3TXfzzSPjHt8Go0FMMP66weoKMgW2PbXlhVKwEuXUHyakLLzewm9UMeQaEiRzhiTMU3UkLXcKbYEJJvfNFcLwSl9W8JCO_l0Yj3ud-qt_nQYEZwqW6u5nfdQllN133iikV4fM5QZsMCnm8Rq1mvLR0y9bmJiD7fwM1tmJ791TUWqmKaTnP49U493VanKpUAfzIiOiIbhg" \ No newline at end of file diff --git a/ingestion/src/metadata/ingestion/source/database/column_type_parser.py b/ingestion/src/metadata/ingestion/source/database/column_type_parser.py index 79cd1c86f0c..c59b431d2e6 100644 --- a/ingestion/src/metadata/ingestion/source/database/column_type_parser.py +++ b/ingestion/src/metadata/ingestion/source/database/column_type_parser.py @@ -276,6 +276,22 @@ class ColumnTypeParser: "WDC_BOOL": "BOOLEAN", "WDC_DATE": "DATE", "WDC_GEOMETRY": "GEOMETRY", + # SAP ERP + "CLNT": "INT", + "INT1": "INT", + "LRAW": "BLOB", + "UNIT": "CHAR", + "NUMC": "CHAR", + "LANG": "CHAR", + "CUKY": "CHAR", + "DATS": "DATE", + "TIMS": "TIME", + "FLTP": "FLOAT", + "QUAN": "DECIMAL", + "DEC": "DECIMAL", + "CURR": "DECIMAL", + "STRG": "STRING", + "RSTR": "STRING", } _COMPLEX_TYPE = re.compile("^(struct|map|array|uniontype)") diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/__init__.py b/ingestion/src/metadata/ingestion/source/database/saperp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/client.py b/ingestion/src/metadata/ingestion/source/database/saperp/client.py new file mode 100644 index 00000000000..a7fa52ad382 --- /dev/null +++ b/ingestion/src/metadata/ingestion/source/database/saperp/client.py @@ -0,0 +1,166 @@ +# Copyright 2021 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Client to interact with SAP ERP APIs +""" + +import math +import time +import traceback +from typing import Any, List, Optional, Union + +from metadata.generated.schema.entity.services.connections.database.sapErpConnection import ( + SapErpConnection, +) +from metadata.ingestion.ometa.client import REST, ClientConfig +from metadata.ingestion.source.database.saperp.models import ( + SapErpColumn, + SapErpColumnResponse, + SapErpTable, + SapErpTableResponse, +) +from metadata.utils.helpers import clean_uri +from metadata.utils.logger import ingestion_logger + +logger = ingestion_logger() + +HEADERS = {"Accept": "*/*"} + + +class SapErpApiException(Exception): + """ + Raise when API returns an error + """ + + +class SapErpClient: + """ + Client to interact with SAP ERP APIs + """ + + def __init__(self, config: SapErpConnection): + self.config: SapErpConnection = config + self.auth_token = self.config.apiKey.get_secret_value() + client_config: ClientConfig = ClientConfig( + base_url=clean_uri(config.hostPort), + auth_header="APIKey", + auth_token_mode="", + auth_token=lambda: (self.auth_token, 0), + api_version="v1", + verify=False, + allow_redirects=True, + ) + self.client = REST(client_config) + + def test_table_api(self): + """ + Check metadata connection to SAS ERP tables API + """ + params_data = {"$top": "1", "$format": "json", "$inlinecount": "allpages"} + response_data = self.client._request( # pylint: disable=protected-access + method="GET", + path="/ECC/DDIC/ZZ_I_DDIC_TAB_CDS/", + headers=HEADERS, + data=params_data, + ) + if response_data: + return response_data + raise SapErpApiException( + "Unable to fetch data from SAP ERP tables API check your connection." + ) + + def test_column_api(self): + """ + Check metadata connection to SAP ERP columns API + """ + params_data = {"$top": "1", "$format": "json", "$inlinecount": "allpages"} + response_data = self.client._request( # pylint: disable=protected-access + method="GET", + path="/ECC/DDIC/ZZ_I_DDIC_COL_CDS/", + headers=HEADERS, + data=params_data, + ) + if response_data: + return response_data + raise SapErpApiException( + "Unable to fetch data from SAP ERP columns API check your connection." + ) + + def paginate( + self, api_url: str, params_data: dict, entities_per_page: int, model_class: Any + ) -> List[Union[SapErpTable, SapErpColumn]]: + """ + Method to paginate the APIs + """ + entities_list = [] + params_data.update({"$top": "1", "$format": "json", "$inlinecount": "allpages"}) + response_data = self.client._request( # pylint: disable=protected-access + method="GET", path=api_url, headers=HEADERS, data=params_data + ) + response = model_class(**response_data) + count = response.d.count + indexes = math.ceil(count / entities_per_page) + for index in range(indexes): + try: + params_data.update( + { + "$top": str(entities_per_page), + "$skip": str(index * entities_per_page), + } + ) + response_data = ( + self.client._request( # pylint: disable=protected-access + method="GET", path=api_url, headers=HEADERS, data=params_data + ) + ) + response = model_class(**response_data) + entities_list.extend(response.d.results) + # Adding a delay before sending the requests to server + # due to server throwing error when too many requests are sent + time.sleep(0.5) + except Exception as exc: + logger.debug(traceback.format_exc()) + logger.warning(f"Error fetching entities for pagination: {exc}") + return entities_list + + def list_tables(self) -> Optional[List[SapErpTable]]: + """ + List all tables on the SAP ERP instance + """ + table_list = [] + params_data = { + "$select": "tabname,tabclass,ddtext", + } + table_list = self.paginate( + api_url="/ECC/DDIC/ZZ_I_DDIC_TAB_CDS/", + params_data=params_data, + entities_per_page=self.config.paginationLimit, + model_class=SapErpTableResponse, + ) + return table_list or None + + def list_columns(self, table_name: str) -> Optional[List[SapErpColumn]]: + """ + List all the columns on the SAP ERP instance + """ + try: + params_data = {"$filter": f"tabname eq '{table_name}'"} + table_columns = self.paginate( + api_url="/ECC/DDIC/ZZ_I_DDIC_COL_CDS/", + params_data=params_data, + entities_per_page=self.config.paginationLimit, + model_class=SapErpColumnResponse, + ) + return table_columns or None + except Exception as exc: + logger.debug(traceback.format_exc()) + logger.warning(f"Error fetching columns for table {table_name}: {exc}") + return None diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/connection.py b/ingestion/src/metadata/ingestion/source/database/saperp/connection.py new file mode 100644 index 00000000000..ee2b2bff902 --- /dev/null +++ b/ingestion/src/metadata/ingestion/source/database/saperp/connection.py @@ -0,0 +1,50 @@ +# Copyright 2021 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Source connection handler +""" +from typing import Optional + +from metadata.generated.schema.entity.automations.workflow import ( + Workflow as AutomationWorkflow, +) +from metadata.generated.schema.entity.services.connections.database.sapErpConnection import ( + SapErpConnection, +) +from metadata.ingestion.connections.test_connections import test_connection_steps +from metadata.ingestion.ometa.ometa_api import OpenMetadata +from metadata.ingestion.source.database.saperp.client import SapErpClient +from metadata.utils.logger import ingestion_logger + +logger = ingestion_logger() + + +def get_connection(connection: SapErpConnection) -> SapErpClient: + return SapErpClient(connection) + + +def test_connection( + metadata: OpenMetadata, + client: SapErpClient, + service_connection: SapErpConnection, + automation_workflow: Optional[AutomationWorkflow] = None, +) -> None: + test_fn = { + "GetTables": client.test_table_api, + "GetColumns": client.test_column_api, + } + test_connection_steps( + metadata=metadata, + test_fn=test_fn, + service_type=service_connection.type.value, + automation_workflow=automation_workflow, + ) diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/constants.py b/ingestion/src/metadata/ingestion/source/database/saperp/constants.py new file mode 100644 index 00000000000..d2052c653ec --- /dev/null +++ b/ingestion/src/metadata/ingestion/source/database/saperp/constants.py @@ -0,0 +1,25 @@ +# Copyright 2023 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Constants Module for SAP ERP +""" + +from metadata.generated.schema.entity.data.table import TableType + +TABLE_TYPE_MAP = { + "TRANSP": TableType.Regular, + "INTTAB": TableType.View, + "CLUSTER": TableType.View, + "POOL": TableType.View, + "VIEW": TableType.View, + "APPEND": TableType.View, +} diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/metadata.py b/ingestion/src/metadata/ingestion/source/database/saperp/metadata.py new file mode 100644 index 00000000000..8f8471895c7 --- /dev/null +++ b/ingestion/src/metadata/ingestion/source/database/saperp/metadata.py @@ -0,0 +1,297 @@ +# Copyright 2021 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +SAP ERP source module +""" +import traceback +from typing import Iterable, List, Optional + +from metadata.generated.schema.api.data.createTable import CreateTableRequest +from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema +from metadata.generated.schema.entity.data.table import ( + Column, + ColumnName, + Constraint, + ConstraintType, + DataType, + Table, + TableConstraint, + TableType, +) +from metadata.generated.schema.entity.services.connections.database.sapErpConnection import ( + SapErpConnection, +) +from metadata.generated.schema.entity.services.ingestionPipelines.status import ( + StackTraceError, +) +from metadata.generated.schema.metadataIngestion.workflow import ( + Source as WorkflowSource, +) +from metadata.generated.schema.type.basic import ( + EntityName, + FullyQualifiedEntityName, + Markdown, +) +from metadata.ingestion.api.models import Either +from metadata.ingestion.api.steps import InvalidSourceException +from metadata.ingestion.ometa.ometa_api import OpenMetadata +from metadata.ingestion.source.database.column_type_parser import ColumnTypeParser +from metadata.ingestion.source.database.common_db_source import CommonDbSourceService +from metadata.ingestion.source.database.saperp.constants import TABLE_TYPE_MAP +from metadata.ingestion.source.database.saperp.models import ( + ColumnsAndConstraints, + SapErpColumn, + SapErpTable, + TableConstraintsModel, +) +from metadata.utils import fqn +from metadata.utils.execution_time_tracker import calculate_execution_time_generator +from metadata.utils.filters import filter_by_table +from metadata.utils.helpers import clean_up_starting_ending_double_quotes_in_string +from metadata.utils.logger import ingestion_logger + +logger = ingestion_logger() + + +class SaperpSource(CommonDbSourceService): + """ + Implements the necessary methods to extract + Database metadata from Sap ERP Source + """ + + @classmethod + def create( + cls, config_dict, metadata: OpenMetadata, pipeline_name: Optional[str] = None + ): + config: WorkflowSource = WorkflowSource.model_validate(config_dict) + connection: SapErpConnection = config.serviceConnection.root.config + if not isinstance(connection, SapErpConnection): + raise InvalidSourceException( + f"Expected SapErpConnection, but got {connection}" + ) + return cls(config, metadata) + + def get_raw_database_schema_names(self) -> Iterable[str]: + if self.service_connection.__dict__.get("databaseSchema"): + yield self.service_connection.databaseSchema + else: + yield "default" + + def get_tables_name_and_type(self) -> Optional[Iterable[SapErpTable]]: + """ + Ingest the tables from SAP ERP + """ + for table in self.connection_obj.list_tables() or []: + try: + table_name = table.tabname + table_type = TABLE_TYPE_MAP.get(table.tabclass, TableType.Regular) + if ( + table_type == TableType.Regular and self.source_config.includeTables + ) or (table_type == TableType.View and self.source_config.includeViews): + table_fqn = fqn.build( + self.metadata, + entity_type=Table, + service_name=self.context.get().database_service, + database_name=self.context.get().database, + schema_name=self.context.get().database_schema, + table_name=table_name, + skip_es_search=True, + ) + if filter_by_table( + self.source_config.tableFilterPattern, + ( + table_fqn + if self.source_config.useFqnForFiltering + else table_name + ), + ): + self.status.filter( + table_fqn, + "Table Filtered Out", + ) + continue + yield table + + except Exception as err: + logger.debug(traceback.format_exc()) + logger.warning( + f"Unable to process table information for table: {str(table_name)} - {err}" + ) + + def _check_col_length( # pylint: disable=arguments-renamed + self, datatype: str + ) -> Optional[int]: + """ + return the column length for the dataLength attribute + """ + if datatype and datatype.upper() in {"CHAR", "VARCHAR", "BINARY", "VARBINARY"}: + return 1 + return None + + def _merge_col_descriptions(self, column: SapErpColumn) -> Optional[str]: + """ + Method to merge the column descriptions from different fields + """ + description = None + try: + if column.scrtext_l: + description = f"**{column.scrtext_l}**" + if column.i_ddtext and column.scrtext_l != column.i_ddtext: + description = f"{description}\n{column.i_ddtext}" + except Exception as exc: + logger.debug(traceback.format_exc()) + logger.warning( + f"Unable to get column descriptions for {column.fieldname}: {exc}" + ) + return description + + def _get_table_constraints( + self, columns: Optional[List[Column]] + ) -> TableConstraintsModel: + """ + Method to get the table constraints + """ + try: + table_constraints = [] + pk_columns = [] + # check if we have multiple primary keys and add them to the TableConstraints + for column in columns or []: + if column.keyflag: + pk_columns.append( + clean_up_starting_ending_double_quotes_in_string( + column.fieldname + ) + ) + if len(pk_columns) > 1: + table_constraints.append( + TableConstraint( + constraintType=ConstraintType.PRIMARY_KEY, + columns=pk_columns, + ) + ) + return TableConstraintsModel( + table_constraints=table_constraints or None, pk_columns=pk_columns + ) + except Exception as exc: + logger.debug(traceback.format_exc()) + logger.warning(f"Failed to fetch table constraints: {exc}") + return TableConstraintsModel() + + def _get_column_constraint(self, column: SapErpColumn, pk_columns: List[str]): + """ + Method to get the column constraint + """ + if column.keyflag: + # In case of multiple primary keys return None for column constraints + # Multiple primary keys will be handled in table constraints + if len(pk_columns) > 1: + return None + return Constraint.PRIMARY_KEY + return Constraint.NOT_NULL if column.notnull == "X" else Constraint.NULL + + def get_columns_and_constraints( # pylint: disable=arguments-differ + self, table_name: str + ) -> ColumnsAndConstraints: + """ + Method to get the column metadata + """ + sap_columns = self.connection_obj.list_columns(table_name) + table_constraints_model = self._get_table_constraints(columns=sap_columns) + om_columns = [] + for sap_column in sap_columns or []: + try: + column_type = ColumnTypeParser.get_column_type(sap_column.datatype) + data_type_display = column_type + column_name = ( + f"{sap_column.fieldname}({sap_column.precfield})" + if sap_column.precfield + else sap_column.fieldname + ) + if sap_column.datatype is None: + column_type = DataType.UNKNOWN.name + data_type_display = column_type.lower() + logger.warning( + f"Unknown type {repr(sap_column.datatype)}: {sap_column.fieldname}" + ) + om_column = Column( + name=ColumnName( + root=column_name + # Passing whitespace if column name is an empty string + # since pydantic doesn't accept empty string + if column_name + else " " + ), + displayName=sap_column.fieldname, + description=self._merge_col_descriptions(column=sap_column), + dataType=column_type, + dataTypeDisplay=data_type_display, + ordinalPosition=int(sap_column.POS), + constraint=self._get_column_constraint( + column=sap_column, pk_columns=table_constraints_model.pk_columns + ), + dataLength=self._check_col_length(datatype=column_type), + ) + if column_type == DataType.ARRAY.value: + om_column.arrayDataType = DataType.UNKNOWN + om_columns.append(om_column) + except Exception as exc: + logger.debug(traceback.format_exc()) + logger.warning( + f"Unable to get column details for {sap_column.fieldname}: {exc}" + ) + return ColumnsAndConstraints( + columns=om_columns, + table_constraints=table_constraints_model.table_constraints, + ) + + # pylint: disable=arguments-renamed + @calculate_execution_time_generator() + def yield_table(self, table: SapErpTable) -> Iterable[Either[CreateTableRequest]]: + """ + From topology. + Prepare a table request and pass it to the sink + """ + schema_name = self.context.get().database_schema + try: + + columns_and_constraints = self.get_columns_and_constraints( + table_name=table.tabname + ) + + table_request = CreateTableRequest( + name=EntityName(table.tabname), + tableType=TABLE_TYPE_MAP.get(table.tabclass, TableType.Regular), + description=Markdown(table.ddtext), + columns=columns_and_constraints.columns, + tableConstraints=columns_and_constraints.table_constraints, + databaseSchema=FullyQualifiedEntityName( + fqn.build( + metadata=self.metadata, + entity_type=DatabaseSchema, + service_name=self.context.get().database_service, + database_name=self.context.get().database, + schema_name=schema_name, + ) + ), + ) + + yield Either(right=table_request) + + # Register the request that we'll handle during the deletion checks + self.register_record(table_request=table_request) + + except Exception as exc: + error = f"Unexpected exception to yield table [{table.tabname}]: {exc}" + yield Either( + left=StackTraceError( + name=table.tabname, error=error, stackTrace=traceback.format_exc() + ) + ) diff --git a/ingestion/src/metadata/ingestion/source/database/saperp/models.py b/ingestion/src/metadata/ingestion/source/database/saperp/models.py new file mode 100644 index 00000000000..1e8ec27cb06 --- /dev/null +++ b/ingestion/src/metadata/ingestion/source/database/saperp/models.py @@ -0,0 +1,98 @@ +# Copyright 2021 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +SAP ERP API models +""" + +from typing import List, Optional + +from pydantic import BaseModel, Field + +from metadata.generated.schema.entity.data.table import Column, TableConstraint + + +class SapErpTable(BaseModel): + """ + SAP ERP Table model + """ + + tabname: str + tabclass: Optional[str] = None + ddtext: Optional[str] = None + + +class SapErpColumn(BaseModel): + """ + SAP ERP Column model + """ + + tabname: str + fieldname: Optional[str] = None + precfield: Optional[str] = None + datatype: Optional[str] = None + POS: Optional[int] = None + notnull: Optional[str] = None + keyflag: Optional[bool] = None + scrtext_l: Optional[str] = None + i_ddtext: Optional[str] = None + dd_text: Optional[str] = None + + +class SapErpTableList(BaseModel): + """ + SAP ERP Table List model + """ + + count: Optional[int] = Field(alias="__count") + results: Optional[List[SapErpTable]] = None + + +class SapErpTableResponse(BaseModel): + """ + SAP ERP Tables Response model + """ + + d: Optional[SapErpTableList] = None + + +class SapErpColumnList(BaseModel): + """ + SAP ERP Column List model + """ + + count: Optional[int] = Field(alias="__count") + results: Optional[List[SapErpColumn]] = None + + +class SapErpColumnResponse(BaseModel): + """ + SAP ERP Columns Response model + """ + + d: Optional[SapErpColumnList] = None + + +class ColumnsAndConstraints(BaseModel): + """ + Wrapper Model for columns and constraints + """ + + columns: Optional[List[Column]] + table_constraints: Optional[List[TableConstraint]] + + +class TableConstraintsModel(BaseModel): + """ + Wrapper Model for table constraints and primary key columns list + """ + + table_constraints: Optional[List[TableConstraint]] = None + pk_columns: List[str] = [] diff --git a/ingestion/tests/unit/resources/datasets/saperp/columns.json b/ingestion/tests/unit/resources/datasets/saperp/columns.json new file mode 100644 index 00000000000..ad5b0bbdb69 --- /dev/null +++ b/ingestion/tests/unit/resources/datasets/saperp/columns.json @@ -0,0 +1,191 @@ +[ + { + "tabname": "T001B_PS", + "inttype": "C", + "intlen": "000004", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000004", + "decimals": "000000", + "domname": "OPVAR", + "fieldname": "BUKRS", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "OPVAR", + "scrtext_l": "Pstng period variant", + "i_ddtext": "Posting Period Variant", + "as4local": "A", + "as4vers": "0000", + "POS": "0003", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "T010O" + }, + { + "tabname": "T001B_PS", + "inttype": "C", + "intlen": "000030", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000030", + "decimals": "000000", + "domname": "FDNAME", + "fieldname": "FIELD", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "FAGL_GLFLEX_FIELDNAME", + "scrtext_l": "GL Field Name", + "i_ddtext": "General Ledger Field Name", + "as4local": "A", + "as4vers": "0000", + "POS": "0004", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "*" + }, + { + "tabname": "T001B_PS", + "inttype": "C", + "intlen": "000003", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CLNT", + "leng": "000003", + "decimals": "000000", + "domname": "MANDT", + "fieldname": "MANDT", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "MANDT", + "scrtext_l": "Client", + "i_ddtext": "Client", + "as4local": "A", + "as4vers": "0000", + "POS": "0001", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "T000" + }, + { + "tabname": "T001B_PS", + "inttype": "C", + "intlen": "000001", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000001", + "decimals": "000000", + "domname": "RRCTY", + "fieldname": "RRCTY", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "RRCTY", + "scrtext_l": "Record Type", + "i_ddtext": "Record Type", + "as4local": "A", + "as4vers": "0000", + "POS": "0002", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "" + }, + { + "tabname": "T001B_PS_PER", + "inttype": "C", + "intlen": "000030", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000030", + "decimals": "000000", + "domname": "MAX_LNG_PSELEMENT", + "fieldname": "BKONT", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "BFM_ETNTITY", + "scrtext_l": "To Account Assmnt", + "i_ddtext": "To Account Assignment", + "as4local": "A", + "as4vers": "0000", + "POS": "0005", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "" + }, + { + "tabname": "T001B_PS_PER", + "inttype": "C", + "intlen": "000004", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000004", + "decimals": "000000", + "domname": "BRGRU", + "fieldname": "BRGRU", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "BRGRU", + "scrtext_l": "Authorization Group", + "i_ddtext": "Authorization Group", + "as4local": "A", + "as4vers": "0000", + "POS": "0015", + "LANG": "E", + "keyflag": false, + "mandatory": false, + "checktable": "*" + }, + { + "tabname": "T001B_PS_PER", + "inttype": "C", + "intlen": "000004", + "reftable": "", + "precfield": "", + "reffield": "", + "notnull": "X", + "datatype": "CHAR", + "leng": "000004", + "decimals": "000000", + "domname": "OPVAR", + "fieldname": "BUKRS", + "comptype": "E", + "reftype": "", + "ddtext": "", + "rollname": "OPVAR", + "scrtext_l": "Pstng period variant", + "i_ddtext": "Posting Period Variant", + "as4local": "A", + "as4vers": "0000", + "POS": "0003", + "LANG": "E", + "keyflag": true, + "mandatory": false, + "checktable": "T010O" + } +] \ No newline at end of file diff --git a/ingestion/tests/unit/resources/datasets/saperp/tables.json b/ingestion/tests/unit/resources/datasets/saperp/tables.json new file mode 100644 index 00000000000..bc4dc447ccd --- /dev/null +++ b/ingestion/tests/unit/resources/datasets/saperp/tables.json @@ -0,0 +1,26 @@ +[ + { + "tabname": "T001B_PS", + "as4local": "A", + "as4vers": "0000", + "LANG": "E", + "tabclass": "TRANSP", + "sqltab": "", + "applclass": "FB", + "authclass": "00", + "masterlang": "", + "ddtext": "Account Assignment Objects in General Ledger" + }, + { + "tabname": "T001B_PS_PER", + "as4local": "A", + "as4vers": "0000", + "LANG": "E", + "tabclass": "TRANSP", + "sqltab": "", + "applclass": "FB", + "authclass": "00", + "masterlang": "", + "ddtext": "Permitted Posting Periods for Account Assignment Objects" + } +] \ No newline at end of file diff --git a/ingestion/tests/unit/topology/database/test_saperp.py b/ingestion/tests/unit/topology/database/test_saperp.py new file mode 100644 index 00000000000..1c919b4ea31 --- /dev/null +++ b/ingestion/tests/unit/topology/database/test_saperp.py @@ -0,0 +1,377 @@ +# Copyright 2024 Collate +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +TestCase for SAP ERP using the topology +""" + +import json +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +from metadata.generated.schema.api.data.createTable import CreateTableRequest +from metadata.generated.schema.entity.data.database import Database +from metadata.generated.schema.entity.data.databaseSchema import DatabaseSchema +from metadata.generated.schema.entity.data.table import ( + Column, + ColumnName, + TableConstraint, +) +from metadata.generated.schema.entity.services.databaseService import ( + DatabaseConnection, + DatabaseService, + DatabaseServiceType, +) +from metadata.generated.schema.metadataIngestion.workflow import ( + OpenMetadataWorkflowConfig, +) +from metadata.generated.schema.type.basic import ( + EntityName, + FullyQualifiedEntityName, + Markdown, +) +from metadata.generated.schema.type.entityReference import EntityReference +from metadata.ingestion.ometa.ometa_api import OpenMetadata +from metadata.ingestion.source.database.saperp.client import SapErpClient +from metadata.ingestion.source.database.saperp.metadata import SaperpSource +from metadata.ingestion.source.database.saperp.models import SapErpColumn, SapErpTable + +mock_saperp_config = { + "source": { + "type": "SapErp", + "serviceName": "local_saperp", + "serviceConnection": { + "config": { + "type": "SapErp", + "hostPort": "https://test.com", + "apiKey": "test_api_key", + "databaseName": "saperp_database", + "databaseSchema": "saperp_database_schema", + "paginationLimit": 100, + } + }, + "sourceConfig": {"config": {"type": "DatabaseMetadata"}}, + }, + "sink": {"type": "metadata-rest", "config": {}}, + "workflowConfig": { + "loggerLevel": "DEBUG", + "openMetadataServerConfig": { + "hostPort": "http://localhost:8585/api", + "authProvider": "openmetadata", + "securityConfig": { + "jwtToken": "eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzQm90IjpmYWxzZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJpYXQiOjE2NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.tS8um_5DKu7HgzGBzS1VTA5uUjKWOCU0B_j08WXBiEC0mr0zNREkqVfwFDD-d24HlNEbrqioLsBuFRiwIWKc1m_ZlVQbG7P36RUxhuv2vbSp80FKyNM-Tj93FDzq91jsyNmsQhyNv_fNr3TXfzzSPjHt8Go0FMMP66weoKMgW2PbXlhVKwEuXUHyakLLzewm9UMeQaEiRzhiTMU3UkLXcKbYEJJvfNFcLwSl9W8JCO_l0Yj3ud-qt_nQYEZwqW6u5nfdQllN133iikV4fM5QZsMCnm8Rq1mvLR0y9bmJiD7fwM1tmJ791TUWqmKaTnP49U493VanKpUAfzIiOiIbhg" + }, + }, + }, +} + +MOCK_DATABASE_SERVICE = DatabaseService( + id="c3eb265f-5445-4ad3-ba5e-797d3a3071bb", + name="saperp_source_test", + connection=DatabaseConnection(), + serviceType=DatabaseServiceType.SapErp, +) + +MOCK_DATABASE = Database( + id="a58b1856-729c-493b-bc87-6d2269b43ec0", + name="saperp_database", + fullyQualifiedName="saperp_source_test.saperp_database", + displayName="saperp_database", + description="", + service=EntityReference( + id="85811038-099a-11ed-861d-0242ac120002", type="databaseService" + ), +) + +MOCK_DATABASE_SCHEMA = DatabaseSchema( + id="c3eb265f-5445-4ad3-ba5e-797d3a3071bb", + name="saperp_database_schema", + fullyQualifiedName="saperp_source_test.saperp_database.saperp_database_schema", + service=EntityReference(id="c3eb265f-5445-4ad3-ba5e-797d3a3071bb", type="database"), + database=EntityReference( + id="a58b1856-729c-493b-bc87-6d2269b43ec0", + type="database", + ), +) + +EXPECTED_TABLES_AND_COLUMNS = [ + CreateTableRequest( + name=EntityName(root="T001B_PS"), + displayName=None, + description=Markdown(root="Account Assignment Objects in General Ledger"), + tableType="Regular", + columns=[ + Column( + name=ColumnName(root="BUKRS"), + displayName="BUKRS", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown( + root="**Pstng period variant**\nPosting Period Variant" + ), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=3, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + Column( + name=ColumnName(root="FIELD"), + displayName="FIELD", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown( + root="**GL Field Name**\nGeneral Ledger Field Name" + ), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=4, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + Column( + name=ColumnName(root="MANDT"), + displayName="MANDT", + dataType="INT", + arrayDataType=None, + dataLength=None, + precision=None, + scale=None, + dataTypeDisplay="INT", + description=Markdown(root="**Client**"), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=1, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + Column( + name=ColumnName(root="RRCTY"), + displayName="RRCTY", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown(root="**Record Type**"), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=2, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + ], + dataModel=None, + tableConstraints=[ + TableConstraint( + constraintType="PRIMARY_KEY", + columns=["BUKRS", "FIELD", "MANDT", "RRCTY"], + referredColumns=None, + ) + ], + tablePartition=None, + tableProfilerConfig=None, + owner=None, + databaseSchema=FullyQualifiedEntityName( + root="saperp_source_test.saperp_database.saperp_database_schema" + ), + tags=None, + schemaDefinition=None, + retentionPeriod=None, + extension=None, + sourceUrl=None, + domain=None, + dataProducts=None, + fileFormat=None, + lifeCycle=None, + sourceHash=None, + ), + CreateTableRequest( + name=EntityName(root="T001B_PS_PER"), + displayName=None, + description=Markdown( + root="Permitted Posting Periods for Account Assignment Objects" + ), + tableType="Regular", + columns=[ + Column( + name=ColumnName(root="BKONT"), + displayName="BKONT", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown( + root="**To Account Assmnt**\nTo Account Assignment" + ), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=5, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + Column( + name=ColumnName(root="BRGRU"), + displayName="BRGRU", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown(root="**Authorization Group**"), + fullyQualifiedName=None, + tags=None, + constraint="NOT_NULL", + ordinalPosition=15, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + Column( + name=ColumnName(root="BUKRS"), + displayName="BUKRS", + dataType="CHAR", + arrayDataType=None, + dataLength=1, + precision=None, + scale=None, + dataTypeDisplay="CHAR", + description=Markdown( + root="**Pstng period variant**\nPosting Period Variant" + ), + fullyQualifiedName=None, + tags=None, + constraint=None, + ordinalPosition=3, + jsonSchema=None, + children=None, + profile=None, + customMetrics=None, + ), + ], + dataModel=None, + tableConstraints=[ + TableConstraint( + constraintType="PRIMARY_KEY", + columns=["BKONT", "BUKRS"], + referredColumns=None, + ) + ], + tablePartition=None, + tableProfilerConfig=None, + owner=None, + databaseSchema=FullyQualifiedEntityName( + root="saperp_source_test.saperp_database.saperp_database_schema" + ), + tags=None, + schemaDefinition=None, + retentionPeriod=None, + extension=None, + sourceUrl=None, + domain=None, + dataProducts=None, + fileFormat=None, + lifeCycle=None, + sourceHash=None, + ), +] + + +def read_datasets(file_name: str) -> dict: + mock_file_path = ( + Path(__file__).parent.parent.parent / f"resources/datasets/saperp/{file_name}" + ) + with open(mock_file_path, encoding="UTF-8") as file: + return json.load(file) + + +def mock_list_tables(self): # pylint: disable=unused-argument + tables = read_datasets("tables.json") + return [SapErpTable(**table) for table in tables] + + +def mock_list_columns(self, table_name: str): # pylint: disable=unused-argument + columns = read_datasets("columns.json") + return [ + SapErpColumn(**column) for column in columns if column["tabname"] == table_name + ] + + +class SapErpUnitTest(TestCase): + """ + Implements the necessary methods to extract + Alation Unit Test + """ + + @patch( + "metadata.ingestion.source.database.saperp.metadata.SaperpSource.test_connection" + ) + def __init__(self, methodName, test_connection) -> None: + super().__init__(methodName) + test_connection.return_value = False + self.config = OpenMetadataWorkflowConfig.model_validate(mock_saperp_config) + self.saperp = SaperpSource.create( + mock_saperp_config["source"], + OpenMetadata(self.config.workflowConfig.openMetadataServerConfig), + ) + self.saperp.context.get().__dict__["database"] = MOCK_DATABASE.name.root + self.saperp.context.get().__dict__[ + "database_service" + ] = MOCK_DATABASE_SERVICE.name.root + self.saperp.context.get().__dict__[ + "database_schema" + ] = MOCK_DATABASE_SCHEMA.name.root + + @patch.object(SapErpClient, "list_tables", mock_list_tables) + @patch.object(SapErpClient, "list_columns", mock_list_columns) + def test_yield_table(self): + """ + Test the yield table + """ + tables = self.saperp.get_tables_name_and_type() + returned_tables = [] + for table in tables: + returned_tables.extend( + [either.right for either in self.saperp.yield_table(table)] + ) + print(returned_tables) + for _, (expected, original) in enumerate( + zip(EXPECTED_TABLES_AND_COLUMNS, returned_tables) + ): + self.assertEqual(expected, original) diff --git a/openmetadata-service/src/main/resources/json/data/testConnections/database/saperp.json b/openmetadata-service/src/main/resources/json/data/testConnections/database/saperp.json new file mode 100644 index 00000000000..7067cdf921e --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/testConnections/database/saperp.json @@ -0,0 +1,20 @@ +{ + "name": "SapErp", + "displayName": "SAP ERP Test Connection", + "description": "This Test Connection validates the access against the database and basic metadata extraction tables and columns.", + "steps": [ + { + "name": "GetTables", + "description": "Validate that we can fetch the list of tables from the API", + "errorMessage": "Failed to fetch tables, please validate if the access token is correct and has enough privilege to fetch tables.", + "mandatory": true + }, + { + "name": "GetColumns", + "description": "Validate that we can fetch the list of tables from the API", + "errorMessage": "Failed to fetch columns, please validate if the access token is correct and has enough privilege to fetch columns.", + "mandatory": false + } + ] +} + diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/database/sapErpConnection.json b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/database/sapErpConnection.json new file mode 100644 index 00000000000..63c33ec767a --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/entity/services/connections/database/sapErpConnection.json @@ -0,0 +1,66 @@ +{ + "$id": "https://open-metadata.org/schema/entity/services/connections/database/sapErpConnection.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SapErpConnection", + "description": "Sap ERP Database Connection Config", + "type": "object", + "javaType": "org.openmetadata.schema.services.connections.database.SapErpConnection", + "definitions": { + "sapErpType": { + "description": "Service type.", + "type": "string", + "enum": ["SapErp"], + "default": "SapErp" + } + }, + "properties": { + "type": { + "title": "Service Type", + "description": "Service Type", + "$ref": "#/definitions/sapErpType", + "default": "SapErp" + }, + "hostPort": { + "expose": true, + "title": "Host and Port", + "description": "Host and Port of the SAP ERP instance.", + "type": "string", + "format": "uri" + }, + "apiKey": { + "title": "API Key", + "description": "API key to authenticate with the SAP ERP APIs.", + "type": "string", + "format": "password" + }, + "databaseName": { + "title": "Database Name", + "description": "Optional name to give to the database in OpenMetadata. If left blank, we will use default as the database name.", + "type": "string" + }, + "databaseSchema": { + "title": "Database Schema", + "description": "Optional name to give to the schema in OpenMetadata. If left blank, we will use default as the schema name", + "type": "string" + }, + "paginationLimit": { + "title": "Pagination Limit", + "description": "Pagination limit used while querying the SAP ERP API for fetching the entities", + "type": "integer", + "default": 10 + }, + "connectionOptions": { + "title": "Connection Options", + "$ref": "../connectionBasicType.json#/definitions/connectionOptions" + }, + "connectionArguments": { + "title": "Connection Arguments", + "$ref": "../connectionBasicType.json#/definitions/connectionArguments" + }, + "supportsMetadataExtraction": { + "title": "Supports Metadata Extraction", + "$ref": "../connectionBasicType.json#/definitions/supportsMetadataExtraction" + } + }, + "additionalProperties": false +} diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/services/databaseService.json b/openmetadata-spec/src/main/resources/json/schema/entity/services/databaseService.json index eaa3ea4c976..869701ca3ab 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/services/databaseService.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/services/databaseService.json @@ -55,7 +55,8 @@ "UnityCatalog", "SAS", "Iceberg", - "Teradata" + "Teradata", + "SapErp" ], "javaEnums": [ { @@ -180,6 +181,9 @@ }, { "name": "Teradata" + }, + { + "name": "SapErp" } ] }, @@ -309,6 +313,9 @@ }, { "$ref": "./connections/database/teradataConnection.json" + }, + { + "$ref": "./connections/database/sapErpConnection.json" } ] } diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/SapErp.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/SapErp.md new file mode 100644 index 00000000000..60a4040c626 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/Database/SapErp.md @@ -0,0 +1,59 @@ +# SAP ERP + +In this section, we provide guides and references to use the SAP ERP connector. + +## Requirements + +You will need the following permissions to extract SAP ERP metadata: + +- **API Access**: You must have the API Enabled permission in your SAP ERP instance. + +## Connection Details + +$$section +### Host Port $(id="hostPort") + +This parameter specifies the host and port of the SAP ERP instance. This should be specified as a string in the format `https://hostname.com`. +$$ + +$$section +### Api Key $(id="apiKey") + +Api Key to authenticate the SAP ERP Apis +$$ + +$$section +### Database Name $(id="databaseName") +In OpenMetadata, the Database Service hierarchy works as follows: +``` +Database Service > Database > Schema > Table +``` +In the case of SAP ERP, we won't have a Database as such. If you'd like to see your data in a database named something other than `default`, you can specify the name in this field. +$$ + +$$section +### Database Schema $(id="databaseSchema") +In OpenMetadata, the Database Service hierarchy works as follows: +``` +Database Service > Database > Schema > Table +``` +In the case of SAP ERP, we won't have a Database Schema as such. If you'd like to see your data in a database schema named something other than `default`, you can specify the name in this field. +$$ + +$$section +### Pagination Limit $(id="paginationLimit") + +Pagination limit used while querying the SAP ERP API for fetching the entities. +$$ + +$$section +### Connection Options $(id="connectionOptions") + +Additional connection options to build the URL that can be sent to service during the connection. +$$ + +$$section +### Connection Arguments $(id="connectionArguments") + +Additional connection arguments such as security or protocol configs that can be sent to service during connection. +$$ diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-sap-erp.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/service-icon-sap-erp.png new file mode 100644 index 0000000000000000000000000000000000000000..180eeaa9dacee3ad5792263884ea5c1a164a1943 GIT binary patch literal 28215 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 X+L$#Q=Fb7Y{Dveis|>1?HV*zDch_HO literal 0 HcmV?d00001 diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerLabel/owner-label.less b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerLabel/owner-label.less index 5354cff1831..263e2ca9f6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerLabel/owner-label.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/OwnerLabel/owner-label.less @@ -20,7 +20,7 @@ display: inline-flex; a { - font-size: 12px; + font-size: 12px; color: @text-color; } diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts index 70e7354a514..84ca779c662 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Services.constant.ts @@ -72,6 +72,7 @@ import redpanda from '../assets/img/service-icon-redpanda.png'; import redshift from '../assets/img/service-icon-redshift.png'; import sagemaker from '../assets/img/service-icon-sagemaker.png'; import salesforce from '../assets/img/service-icon-salesforce.png'; +import sapErp from '../assets/img/service-icon-sap-erp.png'; import sapHana from '../assets/img/service-icon-sap-hana.png'; import sas from '../assets/img/service-icon-sas.svg'; import scikit from '../assets/img/service-icon-scikit.png'; @@ -160,6 +161,7 @@ export const SINGLESTORE = singlestore; export const SALESFORCE = salesforce; export const MLFLOW = mlflow; export const SAP_HANA = sapHana; +export const SAP_ERP = sapErp; export const SCIKIT = scikit; export const DELTALAKE = deltalake; export const DEFAULT_SERVICE = iconDefaultService; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx index 9374bb3b083..776bea9bbfc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddObservabilityPage/AddObservabilityPage.tsx @@ -22,9 +22,9 @@ import Loader from '../../components/common/Loader/Loader'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor'; import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component'; -import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; import { ROUTES, VALIDATION_MESSAGES } from '../../constants/constants'; import { NAME_FIELD_RULES } from '../../constants/Form.constants'; +import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; import { CreateEventSubscription } from '../../generated/events/api/createEventSubscription'; import { AlertType, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.ts index ca52b62cc59..b07f8bd7680 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.ts @@ -44,6 +44,7 @@ import postgresConnection from '../jsons/connectionSchemas/connections/database/ import prestoConnection from '../jsons/connectionSchemas/connections/database/prestoConnection.json'; import redshiftConnection from '../jsons/connectionSchemas/connections/database/redshiftConnection.json'; import salesforceConnection from '../jsons/connectionSchemas/connections/database/salesforceConnection.json'; +import sapErpConnection from '../jsons/connectionSchemas/connections/database/sapErpConnection.json'; import sapHanaConnection from '../jsons/connectionSchemas/connections/database/sapHanaConnection.json'; import sasConnection from '../jsons/connectionSchemas/connections/database/sasConnection.json'; import singleStoreConnection from '../jsons/connectionSchemas/connections/database/singleStoreConnection.json'; @@ -214,6 +215,11 @@ export const getDatabaseConfig = (type: DatabaseServiceType) => { break; } + case DatabaseServiceType.SapERP: { + schema = sapErpConnection; + + break; + } case DatabaseServiceType.MongoDB: { schema = mongoDBConnection; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts index f77d99ec7df..9b2ff9154c2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceUtilClassBase.ts @@ -76,6 +76,7 @@ import { REDSHIFT, SAGEMAKER, SALESFORCE, + SAP_ERP, SAP_HANA, SAS, SCIKIT, @@ -303,6 +304,9 @@ class ServiceUtilClassBase { case this.DatabaseServiceTypeSmallCase.SapHana: return SAP_HANA; + case this.DatabaseServiceTypeSmallCase.SapERP: + return SAP_ERP; + case this.DatabaseServiceTypeSmallCase.DeltaLake: return DELTALAKE;