ui-nyla/src/api/connectionApi.ts
2026-05-19 16:47:52 +02:00

510 lines
14 KiB
TypeScript

import { ApiRequestOptions } from '../hooks/useApi';
// ============================================================================
// TYPES & INTERFACES
// ============================================================================
export interface KnowledgePreferences {
schemaVersion?: number;
mailContentDepth?: 'metadata' | 'snippet' | 'full';
mailIndexAttachments?: boolean;
filesIndexBinaries?: boolean;
clickupScope?: 'titles' | 'title_description' | 'with_comments';
clickupIndexAttachments?: boolean;
maxAgeDays?: number;
}
export interface Connection {
id: string;
userId: string;
authority: 'local' | 'google' | 'msft' | 'clickup' | 'infomaniak';
externalId: string;
externalUsername: string;
externalEmail?: string;
status: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt: number; // Backend uses float for UTC timestamp in seconds
lastChecked: number; // Backend uses float for UTC timestamp in seconds
expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds
knowledgeIngestionEnabled?: boolean;
knowledgePreferences?: KnowledgePreferences | null;
[key: string]: any; // Allow additional properties
}
export interface AttributeDefinition {
name: string;
label: string;
type: 'string' | 'number' | 'date' | 'boolean' | 'enum';
sortable?: boolean;
filterable?: boolean;
searchable?: boolean;
width?: number;
minWidth?: number;
maxWidth?: number;
filterOptions?: string[];
}
export interface PaginationParams {
page?: number;
pageSize?: number;
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record<string, any>;
search?: string;
/** Key of a saved view to apply (server loads groupByLevels, filters, sort from DB). */
viewKey?: string;
/** Explicit grouping levels; when sent (incl. []), overrides the view for this request. */
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
}
export interface GroupBand {
path: string[];
label: string;
startRowIndex: number;
rowCount: number;
}
export interface GroupLayout {
levels: string[];
bands: GroupBand[];
}
export interface PaginatedResponse<T> {
items: T[];
pagination?: {
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
groupLayout?: GroupLayout;
appliedView?: { viewKey?: string; displayName?: string };
}
export interface CreateConnectionData {
id?: string;
userId?: string;
authority?: 'msft' | 'google' | 'clickup' | 'infomaniak';
type?: 'msft' | 'google' | 'clickup' | 'infomaniak'; // Backend maps type → authority
externalId?: string;
externalUsername?: string;
externalEmail?: string;
status?: 'active' | 'expired' | 'revoked' | 'pending';
knowledgeIngestionEnabled?: boolean;
knowledgePreferences?: KnowledgePreferences | null;
connectedAt?: number;
lastChecked?: number;
expiresAt?: number;
}
export interface ConnectResponse {
authUrl: string;
}
// Type for the request function passed to API functions
export type ApiRequestFunction = (options: ApiRequestOptions<any>) => Promise<any>;
// ============================================================================
// API REQUEST FUNCTIONS
// ============================================================================
/**
* Fetch connection attributes from backend
* Endpoint: GET /api/attributes/UserConnection
*/
export async function fetchConnectionAttributes(_request: ApiRequestFunction): Promise<AttributeDefinition[]> {
// Note: This uses api.get directly due to response format handling
// For now, we'll use api.get directly in the hook as well
throw new Error('fetchConnectionAttributes should use api instance directly for response format handling');
}
/**
* Fetch list of connections with optional pagination
* Endpoint: GET /api/connections/
*/
export async function fetchConnections(
request: ApiRequestFunction,
params?: PaginationParams
): Promise<PaginatedResponse<Connection> | Connection[]> {
const requestParams: any = {};
// Build pagination object if provided
if (params) {
const paginationObj: any = {};
if (params.page !== undefined) paginationObj.page = params.page;
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
if (params.viewKey) paginationObj.viewKey = params.viewKey;
if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
}
const data = await request({
url: '/api/connections/',
method: 'get',
params: requestParams
});
return data;
}
/**
* Create a new connection
* Endpoint: POST /api/connections/
*/
export async function createConnection(
request: ApiRequestFunction,
connectionData: CreateConnectionData
): Promise<Connection> {
return await request({
url: '/api/connections/',
method: 'post',
data: connectionData
});
}
/**
* Connect to a service (initiate OAuth)
* Endpoint: POST /api/connections/{connectionId}/connect
*
* @param reauth If true, forces the OAuth provider to re-show the consent screen.
* Required when newly added scopes (e.g. Calendar/Contacts after a
* feature rollout) need to be granted on top of the existing token.
*/
export async function connectService(
request: ApiRequestFunction,
connectionId: string,
reauth: boolean = false
): Promise<ConnectResponse> {
return await request({
url: `/api/connections/${connectionId}/connect`,
method: 'post',
data: reauth ? { reauth: true } : undefined,
});
}
/**
* Disconnect from a service
* Endpoint: POST /api/connections/{connectionId}/disconnect
*/
export async function disconnectService(
request: ApiRequestFunction,
connectionId: string
): Promise<{ message: string }> {
return await request({
url: `/api/connections/${connectionId}/disconnect`,
method: 'post'
});
}
/**
* Delete a connection
* Endpoint: DELETE /api/connections/{connectionId}
*/
export async function deleteConnection(
request: ApiRequestFunction,
connectionId: string
): Promise<{ message: string }> {
return await request({
url: `/api/connections/${connectionId}`,
method: 'delete'
});
}
/**
* Update a connection
* Endpoint: PUT /api/connections/{connectionId}
*/
export async function updateConnection(
request: ApiRequestFunction,
connectionId: string,
updateData: Partial<Connection>
): Promise<Connection> {
return await request({
url: `/api/connections/${connectionId}`,
method: 'put',
data: updateData
});
}
/**
* Refresh Microsoft token
* Endpoint: POST /api/msft/refresh
*/
export async function refreshMicrosoftToken(
request: ApiRequestFunction,
connectionId: string
): Promise<Connection> {
return await request({
url: '/api/msft/refresh',
method: 'post',
data: { connectionId }
});
}
/**
* Refresh Google token
* Endpoint: POST /api/google/refresh
*/
export async function refreshGoogleToken(
request: ApiRequestFunction,
connectionId: string
): Promise<Connection> {
return await request({
url: '/api/google/refresh',
method: 'post',
data: { connectionId }
});
}
/**
* Submit an Infomaniak Personal Access Token (kdrive + mail) for an existing
* UserConnection. The backend validates the token via /1/profile and stores it
* as the connection's data-access bearer token.
* Endpoint: POST /api/infomaniak/connections/{connectionId}/token
*/
export async function submitInfomaniakToken(
request: ApiRequestFunction,
connectionId: string,
token: string
): Promise<{
id: string;
status: string;
type: string;
externalUsername: string;
externalEmail?: string | null;
lastChecked: number;
}> {
return await request({
url: `/api/infomaniak/connections/${connectionId}/token`,
method: 'post',
data: { token }
});
}
// ============================================================================
// RAG KNOWLEDGE CONSENT & CONTROL
// ============================================================================
export async function patchKnowledgeConsent(
request: ApiRequestFunction,
connectionId: string,
enabled: boolean
): Promise<{ connectionId: string; knowledgeIngestionEnabled: boolean; purged?: any; cancelledJobs?: number; bootstrapEnqueued?: boolean }> {
return await request({
url: `/api/connections/${connectionId}/knowledge-consent`,
method: 'patch',
data: { enabled }
});
}
export async function patchKnowledgePreferences(
request: ApiRequestFunction,
connectionId: string,
preferences: KnowledgePreferences
): Promise<{ connectionId: string; knowledgePreferences: KnowledgePreferences; updated: boolean }> {
return await request({
url: `/api/connections/${connectionId}/knowledge-preferences`,
method: 'patch',
data: { preferences }
});
}
export async function postKnowledgeStop(
request: ApiRequestFunction,
connectionId: string
): Promise<{ connectionId: string; cancelled: number }> {
return await request({
url: `/api/connections/${connectionId}/knowledge-stop`,
method: 'post'
});
}
export interface RagLimits {
maxItems?: number;
maxBytes?: number;
maxFileSize?: number;
maxDepth?: number;
// ClickUp variant
maxTasks?: number;
maxWorkspaces?: number;
maxListsPerWorkspace?: number;
}
export interface DataSourceSettings {
ragLimits?: RagLimits;
}
export interface CostEstimate {
estimatedTokens: number;
estimatedUsd: number;
basis: {
kind: string;
limits: Record<string, number>;
assumptions: Record<string, any>;
notes: string;
};
sourceId?: string;
}
export async function patchDataSourceSettings(
request: ApiRequestFunction,
dataSourceId: string,
settings: DataSourceSettings
): Promise<{ sourceId: string; settings: DataSourceSettings; updated: boolean }> {
return await request({
url: `/api/datasources/${dataSourceId}/settings`,
method: 'patch',
data: { settings }
});
}
export async function getDataSourceCostEstimate(
request: ApiRequestFunction,
dataSourceId: string
): Promise<CostEstimate> {
return await request({
url: `/api/datasources/${dataSourceId}/cost-estimate`,
method: 'get'
});
}
export interface PatchFlagResponse {
sourceId: string;
resetDescendantIds: string[];
updatedAncestors: { id: string; [key: string]: any }[];
[key: string]: any;
}
export async function patchDataSourceRagIndex(
request: ApiRequestFunction,
dataSourceId: string,
ragIndexEnabled: boolean | null
): Promise<PatchFlagResponse> {
return await request({
url: `/api/datasources/${dataSourceId}/rag-index`,
method: 'patch',
data: { ragIndexEnabled }
});
}
// ============================================================================
// RAG INVENTORY
// ============================================================================
export interface RagDataSourceDto {
id: string;
label: string;
path: string;
sourceType: string;
/** Three-state inherit semantics on backend; UI reads as effective boolean from RAG inventory aggregator. */
ragIndexEnabled: boolean | null;
neutralize: boolean | null;
lastIndexed: number | null;
/** Distinct files indexed for this DataSource (one row per source document). */
fileCount: number;
/** Embedding-sized text fragments (one per ContentChunk row, ~400 tokens each). */
chunkCount: number;
}
export interface RagConnectionDto {
id: string;
authority: string;
externalEmail: string;
knowledgeIngestionEnabled: boolean;
preferences: KnowledgePreferences;
dataSources: RagDataSourceDto[];
totalFiles: number;
totalChunks: number;
runningJobs: {
jobId: string;
progress: number;
/** Already translated server-side. */
progressMessage: string;
}[];
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
lastSuccess?: {
jobId: string;
finishedAt: number | null;
indexed: number;
skippedDuplicate: number;
skippedPolicy: number;
failed: number;
durationMs: number;
/** Name of the first budget that bit (e.g. "maxBytes", "maxItems", "maxTasks"); null if walk completed naturally. */
stoppedAtLimit?: string | null;
/** Effective limits used by the walker, for showing the value next to the limit name. */
limits?: Record<string, number>;
bytesProcessed?: number;
} | null;
}
export interface RagFeatureDataSourceDto {
id: string;
label: string;
tableName: string;
featureCode: string;
ragIndexEnabled: boolean;
}
export interface RagFeatureInstanceDto {
featureInstanceId: string;
featureCode: string;
label: string;
mandateId: string;
fileCount: number;
chunkCount: number;
statusCounts: Record<string, number>;
dataSources: RagFeatureDataSourceDto[];
ragEnabled: boolean;
runningJobs?: {
jobId: string;
progress: number;
progressMessage: string;
}[];
lastError?: { jobId: string; errorMessage: string; finishedAt: number | null } | null;
lastSuccess?: {
jobId: string;
finishedAt: number | null;
indexed: number;
skippedDuplicate: number;
failed: number;
} | null;
}
export interface RagInventoryDto {
connections: RagConnectionDto[];
featureInstances?: RagFeatureInstanceDto[];
totals: { files: number; chunks: number; bytes?: number };
}
export interface RagActiveJobDto {
jobId: string;
connectionId: string;
connectionLabel?: string;
jobType: string;
progress: number | null;
/** Already translated server-side. */
progressMessage: string;
}
export async function getRagInventoryMe(request: ApiRequestFunction): Promise<RagInventoryDto> {
return await request({ url: '/api/rag/inventory/me', method: 'get' });
}
export async function getRagInventoryMandate(request: ApiRequestFunction): Promise<RagInventoryDto> {
return await request({ url: '/api/rag/inventory/mandate', method: 'get' });
}
export async function getRagInventoryPlatform(request: ApiRequestFunction): Promise<any> {
return await request({ url: '/api/rag/inventory/platform', method: 'get' });
}
export async function getRagActiveJobs(request: ApiRequestFunction): Promise<RagActiveJobDto[]> {
return await request({ url: '/api/rag/inventory/jobs', method: 'get' });
}