mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-01 13:45:44 +00:00
Compare commits
5 Commits
experiment
...
validate-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2e0b9f74 | ||
|
|
a6c9b95001 | ||
|
|
393fd9a5a1 | ||
|
|
bb47c65450 | ||
|
|
15233449ae |
@@ -29,9 +29,11 @@ from onyx.connectors.google_site.connector import GoogleSitesConnector
|
||||
from onyx.connectors.guru.connector import GuruConnector
|
||||
from onyx.connectors.hubspot.connector import HubSpotConnector
|
||||
from onyx.connectors.interfaces import BaseConnector
|
||||
from onyx.connectors.interfaces import ConnectorValidator
|
||||
from onyx.connectors.interfaces import EventConnector
|
||||
from onyx.connectors.interfaces import LoadConnector
|
||||
from onyx.connectors.interfaces import PollConnector
|
||||
from onyx.connectors.interfaces import SlimConnector
|
||||
from onyx.connectors.linear.connector import LinearConnector
|
||||
from onyx.connectors.loopio.connector import LoopioConnector
|
||||
from onyx.connectors.mediawiki.wiki import MediaWikiConnector
|
||||
@@ -57,82 +59,84 @@ class ConnectorMissingException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def identify_connector_class(
|
||||
_CONNECTOR_MAP: dict[DocumentSource, Type[BaseConnector]] = {
|
||||
DocumentSource.WEB: WebConnector,
|
||||
DocumentSource.FILE: LocalFileConnector,
|
||||
DocumentSource.SLACK: SlackPollConnector,
|
||||
DocumentSource.GITHUB: GithubConnector,
|
||||
DocumentSource.GMAIL: GmailConnector,
|
||||
DocumentSource.GITLAB: GitlabConnector,
|
||||
DocumentSource.GOOGLE_DRIVE: GoogleDriveConnector,
|
||||
DocumentSource.BOOKSTACK: BookstackConnector,
|
||||
DocumentSource.CONFLUENCE: ConfluenceConnector,
|
||||
DocumentSource.JIRA: JiraConnector,
|
||||
DocumentSource.PRODUCTBOARD: ProductboardConnector,
|
||||
DocumentSource.SLAB: SlabConnector,
|
||||
DocumentSource.NOTION: NotionConnector,
|
||||
DocumentSource.ZULIP: ZulipConnector,
|
||||
DocumentSource.GURU: GuruConnector,
|
||||
DocumentSource.LINEAR: LinearConnector,
|
||||
DocumentSource.HUBSPOT: HubSpotConnector,
|
||||
DocumentSource.DOCUMENT360: Document360Connector,
|
||||
DocumentSource.GONG: GongConnector,
|
||||
DocumentSource.GOOGLE_SITES: GoogleSitesConnector,
|
||||
DocumentSource.ZENDESK: ZendeskConnector,
|
||||
DocumentSource.LOOPIO: LoopioConnector,
|
||||
DocumentSource.DROPBOX: DropboxConnector,
|
||||
DocumentSource.SHAREPOINT: SharepointConnector,
|
||||
DocumentSource.TEAMS: TeamsConnector,
|
||||
DocumentSource.SALESFORCE: SalesforceConnector,
|
||||
DocumentSource.DISCOURSE: DiscourseConnector,
|
||||
DocumentSource.AXERO: AxeroConnector,
|
||||
DocumentSource.CLICKUP: ClickupConnector,
|
||||
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
||||
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
||||
DocumentSource.ASANA: AsanaConnector,
|
||||
DocumentSource.S3: BlobStorageConnector,
|
||||
DocumentSource.R2: BlobStorageConnector,
|
||||
DocumentSource.GOOGLE_CLOUD_STORAGE: BlobStorageConnector,
|
||||
DocumentSource.OCI_STORAGE: BlobStorageConnector,
|
||||
DocumentSource.XENFORO: XenforoConnector,
|
||||
DocumentSource.DISCORD: DiscordConnector,
|
||||
DocumentSource.FRESHDESK: FreshdeskConnector,
|
||||
DocumentSource.FIREFLIES: FirefliesConnector,
|
||||
DocumentSource.EGNYTE: EgnyteConnector,
|
||||
DocumentSource.AIRTABLE: AirtableConnector,
|
||||
}
|
||||
|
||||
|
||||
class ConnectorCannotHandleInputTypeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_INPUT_TYPE_MAP: dict[
|
||||
InputType,
|
||||
Type[
|
||||
LoadConnector
|
||||
| PollConnector
|
||||
| EventConnector
|
||||
| ConnectorValidator
|
||||
| SlimConnector
|
||||
],
|
||||
] = {
|
||||
InputType.LOAD_STATE: LoadConnector,
|
||||
InputType.POLL: PollConnector,
|
||||
InputType.EVENT: EventConnector,
|
||||
InputType.VALIDATE_CONFIGURATION: ConnectorValidator,
|
||||
InputType.SLIM_RETRIEVAL: SlimConnector,
|
||||
}
|
||||
|
||||
|
||||
def connector_can_handle_type(
|
||||
connector: Type[BaseConnector], input_type: InputType
|
||||
) -> bool:
|
||||
return issubclass(connector, _INPUT_TYPE_MAP[input_type])
|
||||
|
||||
|
||||
def get_connector_class(
|
||||
source: DocumentSource,
|
||||
input_type: InputType | None = None,
|
||||
) -> Type[BaseConnector]:
|
||||
connector_map = {
|
||||
DocumentSource.WEB: WebConnector,
|
||||
DocumentSource.FILE: LocalFileConnector,
|
||||
DocumentSource.SLACK: {
|
||||
InputType.POLL: SlackPollConnector,
|
||||
InputType.SLIM_RETRIEVAL: SlackPollConnector,
|
||||
},
|
||||
DocumentSource.GITHUB: GithubConnector,
|
||||
DocumentSource.GMAIL: GmailConnector,
|
||||
DocumentSource.GITLAB: GitlabConnector,
|
||||
DocumentSource.GOOGLE_DRIVE: GoogleDriveConnector,
|
||||
DocumentSource.BOOKSTACK: BookstackConnector,
|
||||
DocumentSource.CONFLUENCE: ConfluenceConnector,
|
||||
DocumentSource.JIRA: JiraConnector,
|
||||
DocumentSource.PRODUCTBOARD: ProductboardConnector,
|
||||
DocumentSource.SLAB: SlabConnector,
|
||||
DocumentSource.NOTION: NotionConnector,
|
||||
DocumentSource.ZULIP: ZulipConnector,
|
||||
DocumentSource.GURU: GuruConnector,
|
||||
DocumentSource.LINEAR: LinearConnector,
|
||||
DocumentSource.HUBSPOT: HubSpotConnector,
|
||||
DocumentSource.DOCUMENT360: Document360Connector,
|
||||
DocumentSource.GONG: GongConnector,
|
||||
DocumentSource.GOOGLE_SITES: GoogleSitesConnector,
|
||||
DocumentSource.ZENDESK: ZendeskConnector,
|
||||
DocumentSource.LOOPIO: LoopioConnector,
|
||||
DocumentSource.DROPBOX: DropboxConnector,
|
||||
DocumentSource.SHAREPOINT: SharepointConnector,
|
||||
DocumentSource.TEAMS: TeamsConnector,
|
||||
DocumentSource.SALESFORCE: SalesforceConnector,
|
||||
DocumentSource.DISCOURSE: DiscourseConnector,
|
||||
DocumentSource.AXERO: AxeroConnector,
|
||||
DocumentSource.CLICKUP: ClickupConnector,
|
||||
DocumentSource.MEDIAWIKI: MediaWikiConnector,
|
||||
DocumentSource.WIKIPEDIA: WikipediaConnector,
|
||||
DocumentSource.ASANA: AsanaConnector,
|
||||
DocumentSource.S3: BlobStorageConnector,
|
||||
DocumentSource.R2: BlobStorageConnector,
|
||||
DocumentSource.GOOGLE_CLOUD_STORAGE: BlobStorageConnector,
|
||||
DocumentSource.OCI_STORAGE: BlobStorageConnector,
|
||||
DocumentSource.XENFORO: XenforoConnector,
|
||||
DocumentSource.DISCORD: DiscordConnector,
|
||||
DocumentSource.FRESHDESK: FreshdeskConnector,
|
||||
DocumentSource.FIREFLIES: FirefliesConnector,
|
||||
DocumentSource.EGNYTE: EgnyteConnector,
|
||||
DocumentSource.AIRTABLE: AirtableConnector,
|
||||
}
|
||||
connector_by_source = connector_map.get(source, {})
|
||||
|
||||
if isinstance(connector_by_source, dict):
|
||||
if input_type is None:
|
||||
# If not specified, default to most exhaustive update
|
||||
connector = connector_by_source.get(InputType.LOAD_STATE)
|
||||
else:
|
||||
connector = connector_by_source.get(input_type)
|
||||
else:
|
||||
connector = connector_by_source
|
||||
if connector is None:
|
||||
raise ConnectorMissingException(f"Connector not found for source={source}")
|
||||
|
||||
if any(
|
||||
[
|
||||
input_type == InputType.LOAD_STATE
|
||||
and not issubclass(connector, LoadConnector),
|
||||
input_type == InputType.POLL and not issubclass(connector, PollConnector),
|
||||
input_type == InputType.EVENT and not issubclass(connector, EventConnector),
|
||||
]
|
||||
):
|
||||
raise ConnectorMissingException(
|
||||
f"Connector for source={source} does not accept input_type={input_type}"
|
||||
)
|
||||
return connector
|
||||
) -> Type[BaseConnector] | None:
|
||||
return _CONNECTOR_MAP.get(source)
|
||||
|
||||
|
||||
def instantiate_connector(
|
||||
@@ -143,7 +147,14 @@ def instantiate_connector(
|
||||
credential: Credential,
|
||||
tenant_id: str | None = None,
|
||||
) -> BaseConnector:
|
||||
connector_class = identify_connector_class(source, input_type)
|
||||
connector_class = get_connector_class(source)
|
||||
if connector_class is None:
|
||||
raise ConnectorMissingException(f"Connector not found for source={source}")
|
||||
|
||||
if not connector_can_handle_type(connector_class, input_type):
|
||||
raise ConnectorCannotHandleInputTypeException(
|
||||
f"Connector {connector_class} does not accept input_type={input_type}"
|
||||
)
|
||||
|
||||
if source in DocumentSourceRequiringTenantContext:
|
||||
connector_specific_config["tenant_id"] = tenant_id
|
||||
|
||||
@@ -15,6 +15,34 @@ GenerateDocumentsOutput = Iterator[list[Document]]
|
||||
GenerateSlimDocumentOutput = Iterator[list[SlimDocument]]
|
||||
|
||||
|
||||
class BaseConnectorException(Exception):
|
||||
"""Base exception for connector-related errors."""
|
||||
|
||||
def build_error_msg(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class InvalidCredentialsException(BaseConnectorException):
|
||||
"""Exception raised when connector credentials are invalid."""
|
||||
|
||||
def build_error_msg(self) -> str:
|
||||
return f"Invalid credentials:\n {self.args[0]}"
|
||||
|
||||
|
||||
class InvalidConnectorConfigurationException(BaseConnectorException):
|
||||
"""Exception raised when connector configuration is invalid."""
|
||||
|
||||
def build_error_msg(self) -> str:
|
||||
return f"Invalid connector configuration:\n {self.args[0]}"
|
||||
|
||||
|
||||
class InvalidConnectorException(BaseConnectorException):
|
||||
"""Exception raised when validation fails and we arent sure what is invalid."""
|
||||
|
||||
def build_error_msg(self) -> str:
|
||||
return f"Invalid credentials or connector configuration:\n {self.args[0]}"
|
||||
|
||||
|
||||
class BaseConnector(abc.ABC):
|
||||
REDIS_KEY_PREFIX = "da_connector_data:"
|
||||
|
||||
@@ -103,3 +131,12 @@ class EventConnector(BaseConnector):
|
||||
@abc.abstractmethod
|
||||
def handle_event(self, event: Any) -> GenerateDocumentsOutput:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ConnectorValidator(BaseConnector):
|
||||
@abc.abstractmethod
|
||||
def validate_connector_configuration(self) -> None:
|
||||
"""Validates the connector configuration and credentials.
|
||||
Should raise an exception if the configuration is invalid.
|
||||
Otherwise, it should return None."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -15,6 +15,7 @@ class InputType(str, Enum):
|
||||
POLL = "poll" # e.g. calling an API to get all documents in the last hour
|
||||
EVENT = "event" # e.g. registered an endpoint as a listener, and processing connector events
|
||||
SLIM_RETRIEVAL = "slim_retrieval"
|
||||
VALIDATE_CONFIGURATION = "validate_configuration" # for validating connector configuration and credentials
|
||||
|
||||
|
||||
class ConnectorMissingCredentialError(PermissionError):
|
||||
|
||||
@@ -11,6 +11,12 @@ from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.configs.constants import DocumentSourceRequiringTenantContext
|
||||
from onyx.connectors.factory import connector_can_handle_type
|
||||
from onyx.connectors.factory import get_connector_class
|
||||
from onyx.connectors.interfaces import BaseConnectorException
|
||||
from onyx.connectors.interfaces import ConnectorValidator
|
||||
from onyx.connectors.models import InputType
|
||||
from onyx.db.connector import fetch_connector_by_id
|
||||
from onyx.db.credentials import fetch_credential_by_id
|
||||
from onyx.db.enums import AccessType
|
||||
@@ -24,6 +30,7 @@ from onyx.db.models import User
|
||||
from onyx.db.models import User__UserGroup
|
||||
from onyx.db.models import UserGroup__ConnectorCredentialPair
|
||||
from onyx.db.models import UserRole
|
||||
from onyx.server.documents.models import ConnectorCreateAndAssociateRequest
|
||||
from onyx.server.models import StatusResponse
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
|
||||
@@ -85,6 +92,71 @@ def _add_user_filters(
|
||||
return stmt.where(where_clause)
|
||||
|
||||
|
||||
def validate_connector_configuration(
|
||||
connector_data: ConnectorCreateAndAssociateRequest,
|
||||
user: User,
|
||||
db_session: Session,
|
||||
tenant_id: str,
|
||||
) -> None:
|
||||
"""Validates the connector configuration by attempting to initialize and validate the connector.
|
||||
If validation succeeds or if the connector doesn't implement validation, returns success.
|
||||
If validation fails, raises an HTTPException with the error details."""
|
||||
# Get the validator connector class
|
||||
if not (connector_class := get_connector_class(connector_data.source)):
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Connector class not found.",
|
||||
)
|
||||
if not connector_can_handle_type(connector_class, InputType.VALIDATE_CONFIGURATION):
|
||||
# Connector has no validator. Skipping validation.
|
||||
return
|
||||
|
||||
# Get the credential
|
||||
credential = fetch_credential_by_id(
|
||||
credential_id=connector_data.credential_id,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
if not credential:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Credential {connector_data.credential_id} not found",
|
||||
)
|
||||
|
||||
# Initialize the connector
|
||||
if tenant_id and connector_data.source in DocumentSourceRequiringTenantContext:
|
||||
connector_data.connector_specific_config["tenant_id"] = tenant_id
|
||||
|
||||
try:
|
||||
connector = connector_class(**connector_data.connector_specific_config)
|
||||
|
||||
# Load credentials
|
||||
connector.load_credentials(credential.credential_json)
|
||||
|
||||
# This should raise an exception if the configuration is invalid
|
||||
if isinstance(connector, ConnectorValidator):
|
||||
# This should raise an exception if the configuration is invalid
|
||||
connector.validate_connector_configuration()
|
||||
except BaseConnectorException as e:
|
||||
# Handle predicted connector validation errors
|
||||
exception_string = (
|
||||
f"Connector Validation Failed with configuration: {connector_data.connector_specific_config} \n"
|
||||
f"Error: {e}"
|
||||
)
|
||||
logger.exception(exception_string)
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail=e.build_error_msg(),
|
||||
)
|
||||
except Exception as e:
|
||||
# Handle unexpected exceptions during connector validation
|
||||
logger.exception(f"Error validating connector configuration: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error validating connector configuration: {e}",
|
||||
)
|
||||
|
||||
|
||||
def get_connector_credential_pairs(
|
||||
db_session: Session,
|
||||
include_disabled: bool = True,
|
||||
@@ -359,7 +431,7 @@ def add_credential_to_connector(
|
||||
auto_sync_options: dict | None = None,
|
||||
initial_status: ConnectorCredentialPairStatus = ConnectorCredentialPairStatus.ACTIVE,
|
||||
last_successful_index_time: datetime | None = None,
|
||||
) -> StatusResponse:
|
||||
) -> int:
|
||||
connector = fetch_connector_by_id(connector_id, db_session)
|
||||
credential = fetch_credential_by_id(
|
||||
credential_id,
|
||||
@@ -401,10 +473,9 @@ def add_credential_to_connector(
|
||||
.one_or_none()
|
||||
)
|
||||
if existing_association is not None:
|
||||
return StatusResponse(
|
||||
success=False,
|
||||
message=f"Connector {connector_id} already has Credential {credential_id}",
|
||||
data=connector_id,
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Connector {connector_id} already has Credential {credential_id}",
|
||||
)
|
||||
|
||||
association = ConnectorCredentialPair(
|
||||
@@ -429,11 +500,7 @@ def add_credential_to_connector(
|
||||
|
||||
db_session.commit()
|
||||
|
||||
return StatusResponse(
|
||||
success=True,
|
||||
message=f"Creating new association between Connector {connector_id} and Credential {credential_id}",
|
||||
data=association.id,
|
||||
)
|
||||
return association.id
|
||||
|
||||
|
||||
def remove_credential_from_connector(
|
||||
|
||||
@@ -179,7 +179,7 @@ def seed_initial_documents(
|
||||
|
||||
last_index_time = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
result = add_credential_to_connector(
|
||||
cc_pair_id = add_credential_to_connector(
|
||||
db_session=db_session,
|
||||
user=None,
|
||||
connector_id=connector_id,
|
||||
@@ -190,7 +190,6 @@ def seed_initial_documents(
|
||||
initial_status=ConnectorCredentialPairStatus.PAUSED,
|
||||
last_successful_index_time=last_index_time,
|
||||
)
|
||||
cc_pair_id = cast(int, result.data)
|
||||
processed_docs = fetch_versioned_implementation(
|
||||
"onyx.seeding.load_docs",
|
||||
"load_processed_docs",
|
||||
|
||||
@@ -522,7 +522,7 @@ def associate_credential_to_connector(
|
||||
)
|
||||
|
||||
try:
|
||||
response = add_credential_to_connector(
|
||||
cc_pair_id = add_credential_to_connector(
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
connector_id=connector_id,
|
||||
@@ -533,7 +533,11 @@ def associate_credential_to_connector(
|
||||
groups=metadata.groups,
|
||||
)
|
||||
|
||||
return response
|
||||
return StatusResponse(
|
||||
success=True,
|
||||
message="Credential associated successfully",
|
||||
data=cc_pair_id,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
logger.error(f"IntegrityError: {e}")
|
||||
raise HTTPException(status_code=400, detail="Name must be unique")
|
||||
|
||||
@@ -10,6 +10,7 @@ from fastapi import Request
|
||||
from fastapi import Response
|
||||
from fastapi import UploadFile
|
||||
from google.oauth2.credentials import Credentials # type: ignore
|
||||
from psycopg2 import IntegrityError
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -67,6 +68,7 @@ from onyx.db.connector_credential_pair import add_credential_to_connector
|
||||
from onyx.db.connector_credential_pair import get_cc_pair_groups_for_ids
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pairs
|
||||
from onyx.db.connector_credential_pair import validate_connector_configuration
|
||||
from onyx.db.credentials import cleanup_gmail_credentials
|
||||
from onyx.db.credentials import cleanup_google_drive_credentials
|
||||
from onyx.db.credentials import create_credential
|
||||
@@ -93,6 +95,7 @@ from onyx.key_value_store.interface import KvKeyNotFoundError
|
||||
from onyx.redis.redis_connector import RedisConnector
|
||||
from onyx.server.documents.models import AuthStatus
|
||||
from onyx.server.documents.models import AuthUrl
|
||||
from onyx.server.documents.models import ConnectorCreateAndAssociateRequest
|
||||
from onyx.server.documents.models import ConnectorCredentialPairIdentifier
|
||||
from onyx.server.documents.models import ConnectorIndexingStatus
|
||||
from onyx.server.documents.models import ConnectorSnapshot
|
||||
@@ -688,6 +691,81 @@ def _validate_connector_allowed(source: DocumentSource) -> None:
|
||||
)
|
||||
|
||||
|
||||
@router.post("/admin/create-and-link-connector")
|
||||
def create_connector_and_associate_credential(
|
||||
connector_data: ConnectorCreateAndAssociateRequest,
|
||||
user: User = Depends(current_curator_or_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
tenant_id: str = Depends(get_current_tenant_id),
|
||||
) -> StatusResponse:
|
||||
_validate_connector_allowed(connector_data.source)
|
||||
validate_connector_configuration(
|
||||
connector_data=connector_data,
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
try:
|
||||
fetch_ee_implementation_or_noop(
|
||||
"onyx.db.user_group", "validate_object_creation_for_user", None
|
||||
)(
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
target_group_ids=connector_data.groups,
|
||||
object_is_public=connector_data.access_type == AccessType.PUBLIC,
|
||||
object_is_perm_sync=connector_data.access_type == AccessType.SYNC,
|
||||
)
|
||||
connector_base = connector_data.to_connector_base()
|
||||
connector_response = create_connector(
|
||||
db_session=db_session,
|
||||
connector_data=connector_base,
|
||||
)
|
||||
connector_id = int(connector_response.id)
|
||||
|
||||
# If a credential_id is provided, associate it with the connector
|
||||
if connector_data.credential_id is not None:
|
||||
try:
|
||||
cc_pair_id = add_credential_to_connector(
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
connector_id=connector_id,
|
||||
credential_id=connector_data.credential_id,
|
||||
cc_pair_name=connector_data.name,
|
||||
access_type=connector_data.access_type,
|
||||
auto_sync_options=connector_data.auto_sync_options,
|
||||
groups=connector_data.groups,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
# If credential association fails, delete the connector and raise error
|
||||
delete_connector(db_session=db_session, connector_id=connector_id)
|
||||
logger.exception(f"Error associating credential with connector: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Failed to associate credential with connector",
|
||||
)
|
||||
|
||||
create_milestone_and_report(
|
||||
user=user,
|
||||
distinct_id=user.email if user else tenant_id or "N/A",
|
||||
event_type=MilestoneRecordType.CREATED_CONNECTOR,
|
||||
properties=None,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
return StatusResponse(
|
||||
success=True,
|
||||
message="Connector created successfully",
|
||||
data={
|
||||
"cc_pair_id": cc_pair_id,
|
||||
"connector_id": connector_id,
|
||||
},
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error(f"Error creating connector: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/admin/connector")
|
||||
def create_connector_from_model(
|
||||
connector_data: ConnectorUpdateRequest,
|
||||
@@ -761,7 +839,7 @@ def create_connector_with_mock_credential(
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
response = add_credential_to_connector(
|
||||
cc_pair_id = add_credential_to_connector(
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
connector_id=cast(int, connector_response.id), # will aways be an int
|
||||
@@ -779,7 +857,14 @@ def create_connector_with_mock_credential(
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
return response
|
||||
return StatusResponse(
|
||||
success=True,
|
||||
message="Connector created successfully",
|
||||
data={
|
||||
"cc_pair_id": cc_pair_id,
|
||||
"connector_id": connector_response.id,
|
||||
},
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@@ -340,7 +340,7 @@ class ConnectorCredentialPairIdentifier(BaseModel):
|
||||
|
||||
|
||||
class ConnectorCredentialPairMetadata(BaseModel):
|
||||
name: str | None = None
|
||||
name: str
|
||||
access_type: AccessType
|
||||
auto_sync_options: dict[str, Any] | None = None
|
||||
groups: list[int] = Field(default_factory=list)
|
||||
@@ -368,6 +368,12 @@ class CCPropertyUpdateRequest(BaseModel):
|
||||
value: str
|
||||
|
||||
|
||||
class ConnectorCreateAndAssociateRequest(
|
||||
ConnectorUpdateRequest, ConnectorCredentialPairMetadata
|
||||
):
|
||||
credential_id: int
|
||||
|
||||
|
||||
"""Connectors Models"""
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from typing import Generic
|
||||
from typing import Optional
|
||||
from typing import TypeVar
|
||||
from uuid import UUID
|
||||
|
||||
@@ -14,8 +13,8 @@ DataT = TypeVar("DataT")
|
||||
|
||||
class StatusResponse(BaseModel, Generic[DataT]):
|
||||
success: bool
|
||||
message: Optional[str] = None
|
||||
data: Optional[DataT] = None
|
||||
message: str | None = None
|
||||
data: DataT | None = None
|
||||
|
||||
|
||||
class ApiKey(BaseModel):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
import requests
|
||||
|
||||
@@ -12,10 +11,10 @@ def create_connector(
|
||||
name: str,
|
||||
source: str,
|
||||
input_type: str,
|
||||
connector_specific_config: Dict[str, Any],
|
||||
connector_specific_config: dict[str, Any],
|
||||
is_public: bool = True,
|
||||
groups: list[int] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
connector_update_request = {
|
||||
"name": name,
|
||||
"source": source,
|
||||
@@ -37,10 +36,10 @@ def create_connector(
|
||||
def create_credential(
|
||||
name: str,
|
||||
source: str,
|
||||
credential_json: Dict[str, Any],
|
||||
credential_json: dict[str, Any],
|
||||
is_public: bool = True,
|
||||
groups: list[int] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
credential_request = {
|
||||
"name": name,
|
||||
"source": source,
|
||||
@@ -64,7 +63,7 @@ def create_cc_pair(
|
||||
name: str,
|
||||
access_type: str = "public",
|
||||
groups: list[int] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
cc_pair_request = {
|
||||
"name": name,
|
||||
"access_type": access_type,
|
||||
|
||||
@@ -67,6 +67,37 @@ class CCPairManager:
|
||||
connector_specific_config: dict[str, Any] | None = None,
|
||||
credential_json: dict[str, Any] | None = None,
|
||||
user_performing_action: DATestUser | None = None,
|
||||
) -> DATestCCPair:
|
||||
credential = CredentialManager.create(
|
||||
credential_json=credential_json,
|
||||
name=name,
|
||||
source=source,
|
||||
curator_public=(access_type == AccessType.PUBLIC),
|
||||
groups=groups,
|
||||
user_performing_action=user_performing_action,
|
||||
)
|
||||
cc_pair = ConnectorManager.create_and_link_to_credential(
|
||||
credential_id=credential.id,
|
||||
name=name,
|
||||
source=source,
|
||||
input_type=input_type,
|
||||
connector_specific_config=connector_specific_config,
|
||||
access_type=access_type,
|
||||
groups=groups,
|
||||
user_performing_action=user_performing_action,
|
||||
)
|
||||
return cc_pair
|
||||
|
||||
@staticmethod
|
||||
def old_create_from_scratch(
|
||||
name: str | None = None,
|
||||
access_type: AccessType = AccessType.PUBLIC,
|
||||
groups: list[int] | None = None,
|
||||
source: DocumentSource = DocumentSource.FILE,
|
||||
input_type: InputType = InputType.LOAD_STATE,
|
||||
connector_specific_config: dict[str, Any] | None = None,
|
||||
credential_json: dict[str, Any] | None = None,
|
||||
user_performing_action: DATestUser | None = None,
|
||||
) -> DATestCCPair:
|
||||
connector = ConnectorManager.create(
|
||||
name=name,
|
||||
|
||||
@@ -5,15 +5,56 @@ import requests
|
||||
|
||||
from onyx.connectors.models import InputType
|
||||
from onyx.db.enums import AccessType
|
||||
from onyx.server.documents.models import ConnectorCreateAndAssociateRequest
|
||||
from onyx.server.documents.models import ConnectorUpdateRequest
|
||||
from onyx.server.documents.models import DocumentSource
|
||||
from tests.integration.common_utils.constants import API_SERVER_URL
|
||||
from tests.integration.common_utils.constants import GENERAL_HEADERS
|
||||
from tests.integration.common_utils.test_models import DATestCCPair
|
||||
from tests.integration.common_utils.test_models import DATestConnector
|
||||
from tests.integration.common_utils.test_models import DATestUser
|
||||
|
||||
|
||||
class ConnectorManager:
|
||||
@staticmethod
|
||||
def create_and_link_to_credential(
|
||||
credential_id: int,
|
||||
name: str | None = None,
|
||||
source: DocumentSource = DocumentSource.FILE,
|
||||
input_type: InputType = InputType.LOAD_STATE,
|
||||
connector_specific_config: dict[str, Any] | None = None,
|
||||
access_type: AccessType = AccessType.PUBLIC,
|
||||
groups: list[int] | None = None,
|
||||
user_performing_action: DATestUser | None = None,
|
||||
) -> DATestCCPair:
|
||||
name = f"{name}-connector" if name else f"test-connector-{uuid4()}"
|
||||
connector_create_and_associate_request = ConnectorCreateAndAssociateRequest(
|
||||
name=name,
|
||||
source=source,
|
||||
input_type=input_type,
|
||||
connector_specific_config=connector_specific_config or {},
|
||||
access_type=access_type,
|
||||
groups=groups or [],
|
||||
credential_id=credential_id,
|
||||
)
|
||||
response = requests.post(
|
||||
url=f"{API_SERVER_URL}/manage/admin/create-and-link-connector",
|
||||
json=connector_create_and_associate_request.model_dump(),
|
||||
headers=user_performing_action.headers
|
||||
if user_performing_action
|
||||
else GENERAL_HEADERS,
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
return DATestCCPair(
|
||||
id=response_data["data"]["cc_pair_id"],
|
||||
name=name,
|
||||
connector_id=response_data["data"]["connector_id"],
|
||||
credential_id=credential_id,
|
||||
access_type=access_type,
|
||||
groups=groups or [],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
name: str | None = None,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { createConnectorAndAssociateCredential } from "@/lib/connector";
|
||||
import { ValidInputTypes } from "@/lib/types";
|
||||
|
||||
import Title from "@/components/ui/title";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
@@ -11,7 +13,7 @@ import { useFormContext } from "@/components/context/FormContext";
|
||||
import { getSourceDisplayName, getSourceMetadata } from "@/lib/sources";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteCredential, linkCredential } from "@/lib/credential";
|
||||
import { deleteCredential } from "@/lib/credential";
|
||||
import { submitFiles } from "./pages/utils/files";
|
||||
import { submitGoogleSite } from "./pages/utils/google_site";
|
||||
import AdvancedFormPage from "./pages/Advanced";
|
||||
@@ -19,11 +21,7 @@ import DynamicConnectionForm from "./pages/DynamicConnectorCreationForm";
|
||||
import CreateCredential from "@/components/credentials/actions/CreateCredential";
|
||||
import ModifyCredential from "@/components/credentials/actions/ModifyCredential";
|
||||
import { ConfigurableSources, oauthSupportedSources } from "@/lib/types";
|
||||
import {
|
||||
Credential,
|
||||
credentialTemplates,
|
||||
OAuthDetails,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { Credential, credentialTemplates } from "@/lib/connectors/credentials";
|
||||
import {
|
||||
ConnectionConfiguration,
|
||||
connectorConfigs,
|
||||
@@ -32,8 +30,6 @@ import {
|
||||
defaultPruneFreqDays,
|
||||
defaultRefreshFreqMinutes,
|
||||
isLoadState,
|
||||
Connector,
|
||||
ConnectorBase,
|
||||
} from "@/lib/connectors/connectors";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { GmailMain } from "./pages/gmail/GmailPage";
|
||||
@@ -64,62 +60,6 @@ export interface AdvancedConfig {
|
||||
indexingStart: string;
|
||||
}
|
||||
|
||||
const BASE_CONNECTOR_URL = "/api/manage/admin/connector";
|
||||
|
||||
export async function submitConnector<T>(
|
||||
connector: ConnectorBase<T>,
|
||||
connectorId?: number,
|
||||
fakeCredential?: boolean
|
||||
): Promise<{ message: string; isSuccess: boolean; response?: Connector<T> }> {
|
||||
const isUpdate = connectorId !== undefined;
|
||||
if (!connector.connector_specific_config) {
|
||||
connector.connector_specific_config = {} as T;
|
||||
}
|
||||
|
||||
try {
|
||||
if (fakeCredential) {
|
||||
const response = await fetch(
|
||||
"/api/manage/admin/connector-with-mock-credential",
|
||||
{
|
||||
method: isUpdate ? "PATCH" : "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ...connector }),
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
const responseJson = await response.json();
|
||||
return { message: "Success!", isSuccess: true, response: responseJson };
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
return { message: `Error: ${errorData.detail}`, isSuccess: false };
|
||||
}
|
||||
} else {
|
||||
const response = await fetch(
|
||||
BASE_CONNECTOR_URL + (isUpdate ? `/${connectorId}` : ""),
|
||||
{
|
||||
method: isUpdate ? "PATCH" : "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const responseJson = await response.json();
|
||||
return { message: "Success!", isSuccess: true, response: responseJson };
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
return { message: `Error: ${errorData.detail}`, isSuccess: false };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return { message: `Error: ${error}`, isSuccess: false };
|
||||
}
|
||||
}
|
||||
|
||||
export default function AddConnector({
|
||||
connector,
|
||||
}: {
|
||||
@@ -236,8 +176,12 @@ export default function AddConnector({
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
router.push("/admin/indexing/status?message=connector-created");
|
||||
const onSuccess = (cc_pair_id?: number) => {
|
||||
if (cc_pair_id) {
|
||||
router.push(`/admin/connector/${cc_pair_id}`);
|
||||
} else {
|
||||
router.push("/admin/indexing/status?message=connector-created");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthorize = async () => {
|
||||
@@ -372,55 +316,34 @@ export default function AddConnector({
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, isSuccess, response } = await submitConnector<any>(
|
||||
{
|
||||
connector_specific_config: transformedConnectorSpecificConfig,
|
||||
input_type: isLoadState(connector) ? "load_state" : "poll", // single case
|
||||
name: name,
|
||||
source: connector,
|
||||
access_type: access_type,
|
||||
refresh_freq: advancedConfiguration.refreshFreq || null,
|
||||
prune_freq: advancedConfiguration.pruneFreq || null,
|
||||
indexing_start: advancedConfiguration.indexingStart || null,
|
||||
groups: groups,
|
||||
},
|
||||
undefined,
|
||||
credentialActivated ? false : true
|
||||
);
|
||||
// If no credential
|
||||
if (!credentialActivated) {
|
||||
if (isSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
setPopup({ message: message, type: "error" });
|
||||
}
|
||||
}
|
||||
const credential =
|
||||
currentCredential || liveGDriveCredential || liveGmailCredential;
|
||||
|
||||
// Without credential
|
||||
if (credentialActivated && isSuccess && response) {
|
||||
const credential =
|
||||
currentCredential || liveGDriveCredential || liveGmailCredential;
|
||||
const linkCredentialResponse = await linkCredential(
|
||||
response.id,
|
||||
credential?.id!,
|
||||
name,
|
||||
access_type,
|
||||
groups,
|
||||
auto_sync_options
|
||||
);
|
||||
if (linkCredentialResponse.ok) {
|
||||
onSuccess();
|
||||
} else {
|
||||
const errorData = await linkCredentialResponse.json();
|
||||
setPopup({
|
||||
message: errorData.message,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} else if (isSuccess) {
|
||||
onSuccess();
|
||||
const connectorConfig = {
|
||||
connector_specific_config: transformedConnectorSpecificConfig,
|
||||
input_type: (isLoadState(connector)
|
||||
? "load_state"
|
||||
: "poll") as ValidInputTypes,
|
||||
name: name,
|
||||
source: connector,
|
||||
access_type: access_type,
|
||||
refresh_freq: advancedConfiguration.refreshFreq || null,
|
||||
prune_freq: advancedConfiguration.pruneFreq || null,
|
||||
indexing_start: advancedConfiguration.indexingStart || null,
|
||||
groups: groups,
|
||||
credential_id: credential ? credential.id : undefined,
|
||||
auto_sync_options: auto_sync_options,
|
||||
};
|
||||
|
||||
const [error, response] =
|
||||
await createConnectorAndAssociateCredential(connectorConfig);
|
||||
|
||||
if (error) {
|
||||
setPopup({ message: error, type: "error" });
|
||||
} else if (!response.success) {
|
||||
setPopup({ message: response.message, type: "error" });
|
||||
} else {
|
||||
setPopup({ message: message, type: "error" });
|
||||
onSuccess(response.data.cc_pair_id);
|
||||
}
|
||||
return;
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { ValidSources } from "./types";
|
||||
import { ValidSources, AccessType } from "./types";
|
||||
import {
|
||||
Connector,
|
||||
ConnectorBase,
|
||||
@@ -39,6 +39,32 @@ export async function createConnector<T>(
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
export async function createConnectorAndAssociateCredential<T>(
|
||||
connector: ConnectorBase<T> & {
|
||||
credential_id?: number;
|
||||
access_type?: AccessType;
|
||||
groups?: number[];
|
||||
name?: string;
|
||||
auto_sync_options?: Record<string, any>;
|
||||
}
|
||||
): Promise<[string | null, any]> {
|
||||
if (!connector.connector_specific_config) {
|
||||
connector.connector_specific_config = {} as T;
|
||||
}
|
||||
|
||||
const endpoint = connector.credential_id
|
||||
? `/api/manage/admin/create-and-link-connector`
|
||||
: "/api/manage/admin/connector-with-mock-credential";
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(connector),
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
|
||||
export async function updateConnectorCredentialPairName(
|
||||
ccPairId: number,
|
||||
|
||||
Reference in New Issue
Block a user