Compare commits

...

5 Commits

Author SHA1 Message Date
hagen-danswer
6f2e0b9f74 Updated tests to use new endpoint 2025-01-08 15:51:15 -08:00
hagen-danswer
a6c9b95001 Added a connector validation framework 2025-01-08 12:39:36 -08:00
hagen-danswer
393fd9a5a1 mypy 2025-01-08 12:22:32 -08:00
hagen-danswer
bb47c65450 more api updates 2025-01-08 11:57:03 -08:00
hagen-danswer
15233449ae Add an endpoint to create a connector and associate a credential 2025-01-08 07:56:16 -08:00
14 changed files with 445 additions and 216 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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(

View File

@@ -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",

View File

@@ -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")

View File

@@ -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))

View File

@@ -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"""

View File

@@ -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):

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}}

View File

@@ -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,