diff --git a/surfsense_web/atoms/connectors/connector-mutation.atoms.ts b/surfsense_web/atoms/connectors/connector-mutation.atoms.ts new file mode 100644 index 000000000..40ef5adc9 --- /dev/null +++ b/surfsense_web/atoms/connectors/connector-mutation.atoms.ts @@ -0,0 +1,103 @@ +import { atomWithMutation } from "jotai-tanstack-query"; +import { toast } from "sonner"; +import type { + CreateConnectorRequest, + DeleteConnectorRequest, + GetConnectorsResponse, + IndexConnectorRequest, + IndexConnectorResponse, + UpdateConnectorRequest, +} from "@/contracts/types/connector.types"; +import { connectorsApiService } from "@/lib/apis/connectors-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { queryClient } from "@/lib/query-client/client"; +import { activeSearchSpaceIdAtom } from "../search-spaces/search-space-query.atoms"; + +export const createConnectorMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: cacheKeys.connectors.all(searchSpaceId!), + enabled: !!searchSpaceId, + mutationFn: async (request: CreateConnectorRequest) => { + return connectorsApiService.createConnector(request); + }, + + onSuccess: () => { + toast.success("Connector created successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.all(searchSpaceId!), + }); + }, + }; +}); + +export const updateConnectorMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: cacheKeys.connectors.all(searchSpaceId!), + enabled: !!searchSpaceId, + mutationFn: async (request: UpdateConnectorRequest) => { + return connectorsApiService.updateConnector(request); + }, + + onSuccess: (_, request: UpdateConnectorRequest) => { + toast.success("Connector updated successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.all(searchSpaceId!), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.byId(String(request.id)), + }); + }, + }; +}); + +export const deleteConnectorMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: cacheKeys.connectors.all(searchSpaceId!), + enabled: !!searchSpaceId, + mutationFn: async (request: DeleteConnectorRequest) => { + return connectorsApiService.deleteConnector(request); + }, + + onSuccess: (_, request: DeleteConnectorRequest) => { + toast.success("Connector deleted successfully"); + queryClient.setQueryData( + cacheKeys.connectors.all(searchSpaceId!), + (oldData: GetConnectorsResponse | undefined) => { + if (!oldData) return oldData; + return oldData.filter((connector) => connector.id !== request.id); + } + ); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.byId(String(request.id)), + }); + }, + }; +}); + +export const indexConnectorMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: cacheKeys.connectors.index(), + enabled: !!searchSpaceId, + mutationFn: async (request: IndexConnectorRequest) => { + return connectorsApiService.indexConnector(request); + }, + + onSuccess: (response: IndexConnectorResponse) => { + toast.success(response.message); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.all(searchSpaceId!), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.connectors.byId(String(response.connector_id)), + }); + }, + }; +}); diff --git a/surfsense_web/lib/apis/connectors-api.service.ts b/surfsense_web/lib/apis/connectors-api.service.ts new file mode 100644 index 000000000..eeee5e6a1 --- /dev/null +++ b/surfsense_web/lib/apis/connectors-api.service.ts @@ -0,0 +1,200 @@ +import { + type CreateConnectorRequest, + createConnectorRequest, + createConnectorResponse, + type DeleteConnectorRequest, + deleteConnectorRequest, + deleteConnectorResponse, + type GetConnectorRequest, + type GetConnectorsRequest, + getConnectorRequest, + getConnectorResponse, + getConnectorsRequest, + getConnectorsResponse, + type IndexConnectorRequest, + indexConnectorRequest, + indexConnectorResponse, + type ListGitHubRepositoriesRequest, + listGitHubRepositoriesRequest, + listGitHubRepositoriesResponse, + type UpdateConnectorRequest, + updateConnectorRequest, + updateConnectorResponse, +} from "@/contracts/types/connector.types"; +import { ValidationError } from "../error"; +import { baseApiService } from "./base-api.service"; + +class ConnectorsApiService { + /** + * Get all connectors for a search space + */ + getConnectors = async (request: GetConnectorsRequest) => { + const parsedRequest = getConnectorsRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + // Transform query params to be string values + const transformedQueryParams = parsedRequest.data.queryParams + ? Object.fromEntries( + Object.entries(parsedRequest.data.queryParams).map(([k, v]) => { + return [k, String(v)]; + }) + ) + : undefined; + + const queryParams = transformedQueryParams + ? new URLSearchParams(transformedQueryParams).toString() + : ""; + + return baseApiService.get( + `/api/v1/search-source-connectors?${queryParams}`, + getConnectorsResponse + ); + }; + + /** + * Get a single connector by ID + */ + getConnector = async (request: GetConnectorRequest) => { + const parsedRequest = getConnectorRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.get( + `/api/v1/search-source-connectors/${request.id}`, + getConnectorResponse + ); + }; + + /** + * Create a new connector + */ + createConnector = async (request: CreateConnectorRequest) => { + const parsedRequest = createConnectorRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + const { data, queryParams } = parsedRequest.data; + + // Transform query params to be string values + const transformedQueryParams = Object.fromEntries( + Object.entries(queryParams).map(([k, v]) => { + return [k, String(v)]; + }) + ); + + const queryString = new URLSearchParams(transformedQueryParams).toString(); + + return baseApiService.post( + `/api/v1/search-source-connectors?${queryString}`, + createConnectorResponse, + { + body: data, + } + ); + }; + + /** + * Update an existing connector + */ + updateConnector = async (request: UpdateConnectorRequest) => { + const parsedRequest = updateConnectorRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + const { id, data } = parsedRequest.data; + + return baseApiService.put(`/api/v1/search-source-connectors/${id}`, updateConnectorResponse, { + body: data, + }); + }; + + /** + * Delete a connector + */ + deleteConnector = async (request: DeleteConnectorRequest) => { + const parsedRequest = deleteConnectorRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.delete( + `/api/v1/search-source-connectors/${request.id}`, + deleteConnectorResponse + ); + }; + + /** + * Index connector content + */ + indexConnector = async (request: IndexConnectorRequest) => { + const parsedRequest = indexConnectorRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + const { connector_id, queryParams } = parsedRequest.data; + + // Transform query params to be string values + const transformedQueryParams = Object.fromEntries( + Object.entries(queryParams).map(([k, v]) => { + return [k, String(v)]; + }) + ); + + const queryString = new URLSearchParams(transformedQueryParams).toString(); + + return baseApiService.post( + `/api/v1/search-source-connectors/${connector_id}/index?${queryString}`, + indexConnectorResponse + ); + }; + + /** + * List GitHub repositories using a Personal Access Token + */ + listGitHubRepositories = async (request: ListGitHubRepositoriesRequest) => { + const parsedRequest = listGitHubRepositoriesRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.post(`/api/v1/github/repositories`, listGitHubRepositoriesResponse, { + body: parsedRequest.data, + }); + }; +} + +export const connectorsApiService = new ConnectorsApiService(); diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index 31650e33b..94fc1dc1e 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -58,5 +58,6 @@ export const cacheKeys = { withQueryParams: (queries: GetConnectorsRequest["queryParams"]) => ["connectors", ...(queries ? Object.values(queries) : [])] as const, byId: (connectorId: string) => ["connector", connectorId] as const, + index: () => ["connector", "index"] as const, }, };