refactor(admin): split api-client.ts (885 LOC) and endpoints.ts (1262 LOC) into focused modules
api-client.ts is now a thin delegating class (263 LOC) backed by: - api-client-types.ts (84) — shared types, config, FetchContext - api-client-state.ts (120) — state CRUD + export - api-client-projects.ts (160) — project management - api-client-wiki.ts (116) — wiki knowledge base - api-client-operations.ts (299) — checkpoints, flow, modules, UCCA, import, screening endpoints.ts is now a barrel (25 LOC) aggregating the 4 existing domain files (endpoints-python-core, endpoints-python-gdpr, endpoints-python-ops, endpoints-go). All files stay under the 500-line hard cap. Build verified with `npx next build`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
299
admin-compliance/lib/sdk/api-client-operations.ts
Normal file
299
admin-compliance/lib/sdk/api-client-operations.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* SDK API Client — Operational methods.
|
||||
* (checkpoints, flow, modules, UCCA, document import, screening, health)
|
||||
*/
|
||||
|
||||
import {
|
||||
APIResponse,
|
||||
CheckpointValidationResult,
|
||||
FetchContext,
|
||||
CheckpointStatus,
|
||||
} from './api-client-types'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Checkpoint Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a specific checkpoint
|
||||
*/
|
||||
export async function validateCheckpoint(
|
||||
ctx: FetchContext,
|
||||
checkpointId: string,
|
||||
data?: unknown
|
||||
): Promise<CheckpointValidationResult> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
||||
`${ctx.baseUrl}/checkpoints/validate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
checkpointId,
|
||||
data,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw ctx.createError(response.error || 'Checkpoint validation failed', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checkpoint statuses
|
||||
*/
|
||||
export async function getCheckpoints(
|
||||
ctx: FetchContext
|
||||
): Promise<Record<string, CheckpointStatus>> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
||||
`${ctx.baseUrl}/checkpoints?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response.data || {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flow Navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get current flow state
|
||||
*/
|
||||
export async function getFlowState(ctx: FetchContext): Promise<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}>>(
|
||||
`${ctx.baseUrl}/flow?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw ctx.createError('Failed to get flow state', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next/previous step
|
||||
*/
|
||||
export async function navigateFlow(
|
||||
ctx: FetchContext,
|
||||
direction: 'next' | 'previous'
|
||||
): Promise<{ stepId: string; phase: 1 | 2 }> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}>>(
|
||||
`${ctx.baseUrl}/flow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
direction,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw ctx.createError('Failed to navigate flow', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Modules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get available compliance modules from backend
|
||||
*/
|
||||
export async function getModules(
|
||||
ctx: FetchContext,
|
||||
filters?: {
|
||||
serviceType?: string
|
||||
criticality?: string
|
||||
processesPii?: boolean
|
||||
aiComponents?: boolean
|
||||
}
|
||||
): Promise<{ modules: unknown[]; total: number }> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.serviceType) params.set('service_type', filters.serviceType)
|
||||
if (filters?.criticality) params.set('criticality', filters.criticality)
|
||||
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
|
||||
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${ctx.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await ctx.fetchWithRetry<{ modules: unknown[]; total: number }>(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UCCA (Use Case Compliance Assessment)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assess a use case
|
||||
*/
|
||||
export async function assessUseCase(
|
||||
ctx: FetchContext,
|
||||
intake: unknown
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/ucca/assess`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify(intake),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assessments
|
||||
*/
|
||||
export async function getAssessments(ctx: FetchContext): Promise<unknown[]> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown[]>>(
|
||||
`${ctx.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single assessment
|
||||
*/
|
||||
export async function getAssessment(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an assessment
|
||||
*/
|
||||
export async function deleteAssessment(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<APIResponse<void>>(
|
||||
`${ctx.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document Import & Screening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Analyze an uploaded document
|
||||
*/
|
||||
export async function analyzeDocument(
|
||||
ctx: FetchContext,
|
||||
formData: FormData
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/import/analyze`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-Tenant-ID': ctx.tenantId },
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
|
||||
*/
|
||||
export async function scanDependencies(
|
||||
ctx: FetchContext,
|
||||
formData: FormData
|
||||
): Promise<unknown> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${ctx.baseUrl}/screening/scan`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-Tenant-ID': ctx.tenantId },
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
export async function healthCheck(ctx: FetchContext): Promise<boolean> {
|
||||
try {
|
||||
const response = await ctx.fetchWithTimeout(
|
||||
`${ctx.baseUrl}/health`,
|
||||
{ method: 'GET' },
|
||||
`health-${Date.now()}`
|
||||
)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
160
admin-compliance/lib/sdk/api-client-projects.ts
Normal file
160
admin-compliance/lib/sdk/api-client-projects.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* SDK API Client — Project management methods.
|
||||
* (listProjects, createProject, updateProject, getProject,
|
||||
* archiveProject, restoreProject, permanentlyDeleteProject)
|
||||
*/
|
||||
|
||||
import { FetchContext } from './api-client-types'
|
||||
import { ProjectInfo } from './types'
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
export async function listProjects(
|
||||
ctx: FetchContext,
|
||||
includeArchived = true
|
||||
): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await ctx.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}&include_archived=${includeArchived}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
export async function createProject(
|
||||
ctx: FetchContext,
|
||||
data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: ctx.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
export async function updateProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string,
|
||||
data: { name?: string; description?: string }
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: ctx.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project by ID
|
||||
*/
|
||||
export async function getProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
export async function archiveProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<{ success: boolean }>(
|
||||
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an archived project
|
||||
*/
|
||||
export async function restoreProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<ProjectInfo> {
|
||||
const response = await ctx.fetchWithRetry<ProjectInfo>(
|
||||
`${ctx.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a project and all data
|
||||
*/
|
||||
export async function permanentlyDeleteProject(
|
||||
ctx: FetchContext,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
await ctx.fetchWithRetry<{ success: boolean }>(
|
||||
`${ctx.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': ctx.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
120
admin-compliance/lib/sdk/api-client-state.ts
Normal file
120
admin-compliance/lib/sdk/api-client-state.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* SDK API Client — State management methods.
|
||||
* (getState, saveState, deleteState, exportState)
|
||||
*/
|
||||
|
||||
import {
|
||||
APIResponse,
|
||||
APIError,
|
||||
StateResponse,
|
||||
FetchContext,
|
||||
SDKState,
|
||||
} from './api-client-types'
|
||||
|
||||
/**
|
||||
* Load SDK state for the current tenant
|
||||
*/
|
||||
export async function getState(ctx: FetchContext): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: ctx.tenantId })
|
||||
if (ctx.projectId) params.set('projectId', ctx.projectId)
|
||||
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${ctx.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
const apiError = error as APIError
|
||||
// 404 means no state exists yet - that's okay
|
||||
if (apiError.status === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SDK state for the current tenant.
|
||||
* Supports optimistic locking via version parameter.
|
||||
*/
|
||||
export async function saveState(
|
||||
ctx: FetchContext,
|
||||
state: SDKState,
|
||||
version?: number
|
||||
): Promise<StateResponse> {
|
||||
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${ctx.baseUrl}/state`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(version !== undefined && { 'If-Match': String(version) }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: ctx.tenantId,
|
||||
projectId: ctx.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success) {
|
||||
throw ctx.createError(response.error || 'Failed to save state', 500, true)
|
||||
}
|
||||
|
||||
return response.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
export async function deleteState(ctx: FetchContext): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: ctx.tenantId })
|
||||
if (ctx.projectId) params.set('projectId', ctx.projectId)
|
||||
await ctx.fetchWithRetry<APIResponse<void>>(
|
||||
`${ctx.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export SDK state in various formats
|
||||
*/
|
||||
export async function exportState(
|
||||
ctx: FetchContext,
|
||||
format: 'json' | 'pdf' | 'zip'
|
||||
): Promise<Blob> {
|
||||
const response = await ctx.fetchWithTimeout(
|
||||
`${ctx.baseUrl}/export?tenantId=${encodeURIComponent(ctx.tenantId)}&format=${format}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept':
|
||||
format === 'json'
|
||||
? 'application/json'
|
||||
: format === 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'application/zip',
|
||||
},
|
||||
},
|
||||
`export-${Date.now()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw ctx.createError(`Export failed: ${response.statusText}`, response.status, true)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
84
admin-compliance/lib/sdk/api-client-types.ts
Normal file
84
admin-compliance/lib/sdk/api-client-types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* SDK API Client — shared types, interfaces, and configuration constants.
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface APIResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
version?: number
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface StateResponse {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version: number
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
export interface SaveStateRequest {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version?: number // For optimistic locking
|
||||
}
|
||||
|
||||
export interface CheckpointValidationResult {
|
||||
checkpointId: string
|
||||
passed: boolean
|
||||
errors: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
warnings: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
validatedAt: string
|
||||
validatedBy: string
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
retryable: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_BASE_URL = '/api/sdk/v1'
|
||||
export const DEFAULT_TIMEOUT = 30000 // 30 seconds
|
||||
export const MAX_RETRIES = 3
|
||||
export const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
|
||||
// =============================================================================
|
||||
// FETCH CONTEXT — passed to domain helpers
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Subset of the SDKApiClient that domain helpers need to make requests.
|
||||
* Avoids exposing the entire class and keeps helpers unit-testable.
|
||||
*/
|
||||
export interface FetchContext {
|
||||
baseUrl: string
|
||||
tenantId: string
|
||||
projectId: string | undefined
|
||||
fetchWithRetry<T>(url: string, options: RequestInit, retries?: number): Promise<T>
|
||||
fetchWithTimeout(url: string, options: RequestInit, requestId: string): Promise<Response>
|
||||
createError(message: string, status?: number, retryable?: boolean): APIError
|
||||
}
|
||||
|
||||
// Re-export types that domain helpers need from ./types
|
||||
export type { SDKState, CheckpointStatus }
|
||||
116
admin-compliance/lib/sdk/api-client-wiki.ts
Normal file
116
admin-compliance/lib/sdk/api-client-wiki.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* SDK API Client — Wiki (read-only knowledge base) methods.
|
||||
* (listWikiCategories, listWikiArticles, getWikiArticle, searchWiki)
|
||||
*/
|
||||
|
||||
import { FetchContext } from './api-client-types'
|
||||
import { WikiCategory, WikiArticle, WikiSearchResult } from './types'
|
||||
|
||||
/**
|
||||
* List all wiki categories with article counts
|
||||
*/
|
||||
export async function listWikiCategories(ctx: FetchContext): Promise<WikiCategory[]> {
|
||||
const data = await ctx.fetchWithRetry<{ categories: Array<{
|
||||
id: string; name: string; description: string; icon: string;
|
||||
sort_order: number; article_count: number
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=categories`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.categories || []).map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
icon: c.icon,
|
||||
sortOrder: c.sort_order,
|
||||
articleCount: c.article_count,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* List wiki articles, optionally filtered by category
|
||||
*/
|
||||
export async function listWikiArticles(
|
||||
ctx: FetchContext,
|
||||
categoryId?: string
|
||||
): Promise<WikiArticle[]> {
|
||||
const params = new URLSearchParams({ endpoint: 'articles' })
|
||||
if (categoryId) params.set('category_id', categoryId)
|
||||
const data = await ctx.fetchWithRetry<{ articles: Array<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?${params.toString()}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.articles || []).map(a => ({
|
||||
id: a.id,
|
||||
categoryId: a.category_id,
|
||||
categoryName: a.category_name,
|
||||
title: a.title,
|
||||
summary: a.summary,
|
||||
content: a.content,
|
||||
legalRefs: a.legal_refs || [],
|
||||
tags: a.tags || [],
|
||||
relevance: a.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: a.source_urls || [],
|
||||
version: a.version,
|
||||
updatedAt: a.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single wiki article by ID
|
||||
*/
|
||||
export async function getWikiArticle(
|
||||
ctx: FetchContext,
|
||||
id: string
|
||||
): Promise<WikiArticle> {
|
||||
const data = await ctx.fetchWithRetry<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return {
|
||||
id: data.id,
|
||||
categoryId: data.category_id,
|
||||
categoryName: data.category_name,
|
||||
title: data.title,
|
||||
summary: data.summary,
|
||||
content: data.content,
|
||||
legalRefs: data.legal_refs || [],
|
||||
tags: data.tags || [],
|
||||
relevance: data.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: data.source_urls || [],
|
||||
version: data.version,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text search across wiki articles
|
||||
*/
|
||||
export async function searchWiki(
|
||||
ctx: FetchContext,
|
||||
query: string
|
||||
): Promise<WikiSearchResult[]> {
|
||||
const data = await ctx.fetchWithRetry<{ results: Array<{
|
||||
id: string; title: string; summary: string; category_name: string;
|
||||
relevance: string; highlight: string
|
||||
}> }>(
|
||||
`${ctx.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.results || []).map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
categoryName: r.category_name,
|
||||
relevance: r.relevance,
|
||||
highlight: r.highlight,
|
||||
}))
|
||||
}
|
||||
@@ -3,68 +3,36 @@
|
||||
*
|
||||
* Centralized API client for SDK state management with error handling,
|
||||
* retry logic, and optimistic locking support.
|
||||
*
|
||||
* Domain methods are implemented in sibling files and delegated to here:
|
||||
* api-client-state.ts — getState, saveState, deleteState, exportState
|
||||
* api-client-projects.ts — listProjects … permanentlyDeleteProject
|
||||
* api-client-wiki.ts — listWikiCategories … searchWiki
|
||||
* api-client-operations.ts — checkpoints, flow, modules, UCCA, import, screening
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus, ProjectInfo, WikiCategory, WikiArticle, WikiSearchResult } from './types'
|
||||
import {
|
||||
APIResponse,
|
||||
StateResponse,
|
||||
SaveStateRequest,
|
||||
CheckpointValidationResult,
|
||||
APIError,
|
||||
FetchContext,
|
||||
DEFAULT_BASE_URL,
|
||||
DEFAULT_TIMEOUT,
|
||||
MAX_RETRIES,
|
||||
RETRY_DELAYS,
|
||||
} from './api-client-types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
// Re-export public types so existing consumers keep working
|
||||
export type { APIResponse, StateResponse, SaveStateRequest, CheckpointValidationResult, APIError }
|
||||
|
||||
export interface APIResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
version?: number
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface StateResponse {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version: number
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
export interface SaveStateRequest {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version?: number // For optimistic locking
|
||||
}
|
||||
|
||||
export interface CheckpointValidationResult {
|
||||
checkpointId: string
|
||||
passed: boolean
|
||||
errors: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
warnings: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
validatedAt: string
|
||||
validatedBy: string
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
retryable: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const DEFAULT_BASE_URL = '/api/sdk/v1'
|
||||
const DEFAULT_TIMEOUT = 30000 // 30 seconds
|
||||
const MAX_RETRIES = 3
|
||||
const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
// Domain helpers
|
||||
import * as stateHelpers from './api-client-state'
|
||||
import * as projectHelpers from './api-client-projects'
|
||||
import * as wikiHelpers from './api-client-wiki'
|
||||
import * as opsHelpers from './api-client-operations'
|
||||
|
||||
// =============================================================================
|
||||
// API CLIENT
|
||||
@@ -90,17 +58,17 @@ export class SDKApiClient {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// Private infrastructure — also exposed via FetchContext to helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private createError(message: string, status?: number, retryable = false): APIError {
|
||||
createError(message: string, status?: number, retryable = false): APIError {
|
||||
const error = new Error(message) as APIError
|
||||
error.status = status
|
||||
error.retryable = retryable
|
||||
return error
|
||||
}
|
||||
|
||||
private async fetchWithTimeout(
|
||||
async fetchWithTimeout(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
requestId: string
|
||||
@@ -122,7 +90,7 @@ export class SDKApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWithRetry<T>(
|
||||
async fetchWithRetry<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
retries = MAX_RETRIES
|
||||
@@ -182,673 +150,83 @@ export class SDKApiClient {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - State Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load SDK state for the current tenant
|
||||
*/
|
||||
async getState(): Promise<StateResponse | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
const apiError = error as APIError
|
||||
// 404 means no state exists yet - that's okay
|
||||
if (apiError.status === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
/** Build a FetchContext for passing to domain helpers */
|
||||
private get ctx(): FetchContext {
|
||||
return {
|
||||
baseUrl: this.baseUrl,
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
fetchWithRetry: this.fetchWithRetry.bind(this),
|
||||
fetchWithTimeout: this.fetchWithTimeout.bind(this),
|
||||
createError: this.createError.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SDK state for the current tenant
|
||||
* Supports optimistic locking via version parameter
|
||||
*/
|
||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(version !== undefined && { 'If-Match': String(version) }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
projectId: this.projectId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success) {
|
||||
throw this.createError(response.error || 'Failed to save state', 500, true)
|
||||
}
|
||||
|
||||
return response.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
async deleteState(): Promise<void> {
|
||||
const params = new URLSearchParams({ tenantId: this.tenantId })
|
||||
if (this.projectId) params.set('projectId', this.projectId)
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/state?${params.toString()}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Checkpoint Validation
|
||||
// State Management (api-client-state.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a specific checkpoint
|
||||
*/
|
||||
async validateCheckpoint(
|
||||
checkpointId: string,
|
||||
data?: unknown
|
||||
): Promise<CheckpointValidationResult> {
|
||||
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
||||
`${this.baseUrl}/checkpoints/validate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
checkpointId,
|
||||
data,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw this.createError(response.error || 'Checkpoint validation failed', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checkpoint statuses
|
||||
*/
|
||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
||||
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
||||
`${this.baseUrl}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response.data || {}
|
||||
}
|
||||
async getState(): Promise<StateResponse | null> { return stateHelpers.getState(this.ctx) }
|
||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> { return stateHelpers.saveState(this.ctx, state, version) }
|
||||
async deleteState(): Promise<void> { return stateHelpers.deleteState(this.ctx) }
|
||||
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> { return stateHelpers.exportState(this.ctx, format) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Flow Navigation
|
||||
// Checkpoints & Flow (api-client-operations.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get current flow state
|
||||
*/
|
||||
async getFlowState(): Promise<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}>>(
|
||||
`${this.baseUrl}/flow?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to get flow state', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next/previous step
|
||||
*/
|
||||
async navigateFlow(direction: 'next' | 'previous'): Promise<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}>>(
|
||||
`${this.baseUrl}/flow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
direction,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to navigate flow', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
async validateCheckpoint(checkpointId: string, data?: unknown): Promise<CheckpointValidationResult> { return opsHelpers.validateCheckpoint(this.ctx, checkpointId, data) }
|
||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> { return opsHelpers.getCheckpoints(this.ctx) }
|
||||
async getFlowState() { return opsHelpers.getFlowState(this.ctx) }
|
||||
async navigateFlow(direction: 'next' | 'previous') { return opsHelpers.navigateFlow(this.ctx, direction) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Modules
|
||||
// Modules, UCCA, Import, Screening, Health (api-client-operations.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get available compliance modules from backend
|
||||
*/
|
||||
async getModules(filters?: {
|
||||
serviceType?: string
|
||||
criticality?: string
|
||||
processesPii?: boolean
|
||||
aiComponents?: boolean
|
||||
}): Promise<{ modules: unknown[]; total: number }> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.serviceType) params.set('service_type', filters.serviceType)
|
||||
if (filters?.criticality) params.set('criticality', filters.criticality)
|
||||
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
|
||||
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${this.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await this.fetchWithRetry<{ modules: unknown[]; total: number }>(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
async getModules(filters?: Parameters<typeof opsHelpers.getModules>[1]) { return opsHelpers.getModules(this.ctx, filters) }
|
||||
async assessUseCase(intake: unknown) { return opsHelpers.assessUseCase(this.ctx, intake) }
|
||||
async getAssessments() { return opsHelpers.getAssessments(this.ctx) }
|
||||
async getAssessment(id: string) { return opsHelpers.getAssessment(this.ctx, id) }
|
||||
async deleteAssessment(id: string) { return opsHelpers.deleteAssessment(this.ctx, id) }
|
||||
async analyzeDocument(formData: FormData) { return opsHelpers.analyzeDocument(this.ctx, formData) }
|
||||
async scanDependencies(formData: FormData) { return opsHelpers.scanDependencies(this.ctx, formData) }
|
||||
async healthCheck() { return opsHelpers.healthCheck(this.ctx) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - UCCA (Use Case Compliance Assessment)
|
||||
// Projects (api-client-projects.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assess a use case
|
||||
*/
|
||||
async assessUseCase(intake: unknown): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/ucca/assess`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify(intake),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assessments
|
||||
*/
|
||||
async getAssessments(): Promise<unknown[]> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown[]>>(
|
||||
`${this.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single assessment
|
||||
*/
|
||||
async getAssessment(id: string): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an assessment
|
||||
*/
|
||||
async deleteAssessment(id: string): Promise<void> {
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/ucca/assessments/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
async listProjects(includeArchived = true) { return projectHelpers.listProjects(this.ctx, includeArchived) }
|
||||
async createProject(data: Parameters<typeof projectHelpers.createProject>[1]) { return projectHelpers.createProject(this.ctx, data) }
|
||||
async updateProject(projectId: string, data: Parameters<typeof projectHelpers.updateProject>[2]) { return projectHelpers.updateProject(this.ctx, projectId, data) }
|
||||
async getProject(projectId: string) { return projectHelpers.getProject(this.ctx, projectId) }
|
||||
async archiveProject(projectId: string) { return projectHelpers.archiveProject(this.ctx, projectId) }
|
||||
async restoreProject(projectId: string) { return projectHelpers.restoreProject(this.ctx, projectId) }
|
||||
async permanentlyDeleteProject(projectId: string) { return projectHelpers.permanentlyDeleteProject(this.ctx, projectId) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Document Import
|
||||
// Wiki (api-client-wiki.ts)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Analyze an uploaded document
|
||||
*/
|
||||
async analyzeDocument(formData: FormData): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/import/analyze`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
async listWikiCategories() { return wikiHelpers.listWikiCategories(this.ctx) }
|
||||
async listWikiArticles(categoryId?: string) { return wikiHelpers.listWikiArticles(this.ctx, categoryId) }
|
||||
async getWikiArticle(id: string) { return wikiHelpers.getWikiArticle(this.ctx, id) }
|
||||
async searchWiki(query: string) { return wikiHelpers.searchWiki(this.ctx, query) }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - System Screening
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
|
||||
*/
|
||||
async scanDependencies(formData: FormData): Promise<unknown> {
|
||||
const response = await this.fetchWithRetry<APIResponse<unknown>>(
|
||||
`${this.baseUrl}/screening/scan`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Export SDK state in various formats
|
||||
*/
|
||||
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/zip',
|
||||
},
|
||||
},
|
||||
`export-${Date.now()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createError(`Export failed: ${response.statusText}`, response.status, true)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cancel all pending requests
|
||||
*/
|
||||
cancelAllRequests(): void {
|
||||
this.abortControllers.forEach(controller => controller.abort())
|
||||
this.abortControllers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tenant ID (useful when switching contexts)
|
||||
*/
|
||||
setTenantId(tenantId: string): void {
|
||||
this.tenantId = tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tenant ID
|
||||
*/
|
||||
getTenantId(): string {
|
||||
return this.tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project ID for multi-project support
|
||||
*/
|
||||
setProjectId(projectId: string | undefined): void {
|
||||
this.projectId = projectId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current project ID
|
||||
*/
|
||||
getProjectId(): string | undefined {
|
||||
return this.projectId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Project Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List all projects for the current tenant
|
||||
*/
|
||||
async listProjects(includeArchived = true): Promise<{ projects: ProjectInfo[]; total: number }> {
|
||||
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}&include_archived=${includeArchived}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async createProject(data: {
|
||||
name: string
|
||||
description?: string
|
||||
customer_type?: string
|
||||
copy_from_project_id?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing project
|
||||
*/
|
||||
async updateProject(projectId: string, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
}): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...data,
|
||||
tenant_id: this.tenantId,
|
||||
}),
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project by ID
|
||||
*/
|
||||
async getProject(projectId: string): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive (soft-delete) a project
|
||||
*/
|
||||
async archiveProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an archived project
|
||||
*/
|
||||
async restoreProject(projectId: string): Promise<ProjectInfo> {
|
||||
const response = await this.fetchWithRetry<ProjectInfo>(
|
||||
`${this.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a project and all data
|
||||
*/
|
||||
async permanentlyDeleteProject(projectId: string): Promise<void> {
|
||||
await this.fetchWithRetry<{ success: boolean }>(
|
||||
`${this.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': this.tenantId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WIKI (read-only knowledge base)
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* List all wiki categories with article counts
|
||||
*/
|
||||
async listWikiCategories(): Promise<WikiCategory[]> {
|
||||
const data = await this.fetchWithRetry<{ categories: Array<{
|
||||
id: string; name: string; description: string; icon: string;
|
||||
sort_order: number; article_count: number
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?endpoint=categories`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.categories || []).map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
icon: c.icon,
|
||||
sortOrder: c.sort_order,
|
||||
articleCount: c.article_count,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* List wiki articles, optionally filtered by category
|
||||
*/
|
||||
async listWikiArticles(categoryId?: string): Promise<WikiArticle[]> {
|
||||
const params = new URLSearchParams({ endpoint: 'articles' })
|
||||
if (categoryId) params.set('category_id', categoryId)
|
||||
const data = await this.fetchWithRetry<{ articles: Array<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?${params.toString()}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.articles || []).map(a => ({
|
||||
id: a.id,
|
||||
categoryId: a.category_id,
|
||||
categoryName: a.category_name,
|
||||
title: a.title,
|
||||
summary: a.summary,
|
||||
content: a.content,
|
||||
legalRefs: a.legal_refs || [],
|
||||
tags: a.tags || [],
|
||||
relevance: a.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: a.source_urls || [],
|
||||
version: a.version,
|
||||
updatedAt: a.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single wiki article by ID
|
||||
*/
|
||||
async getWikiArticle(id: string): Promise<WikiArticle> {
|
||||
const data = await this.fetchWithRetry<{
|
||||
id: string; category_id: string; category_name: string; title: string;
|
||||
summary: string; content: string; legal_refs: string[]; tags: string[];
|
||||
relevance: string; source_urls: string[]; version: number; updated_at: string
|
||||
}>(
|
||||
`${this.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return {
|
||||
id: data.id,
|
||||
categoryId: data.category_id,
|
||||
categoryName: data.category_name,
|
||||
title: data.title,
|
||||
summary: data.summary,
|
||||
content: data.content,
|
||||
legalRefs: data.legal_refs || [],
|
||||
tags: data.tags || [],
|
||||
relevance: data.relevance as WikiArticle['relevance'],
|
||||
sourceUrls: data.source_urls || [],
|
||||
version: data.version,
|
||||
updatedAt: data.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-text search across wiki articles
|
||||
*/
|
||||
async searchWiki(query: string): Promise<WikiSearchResult[]> {
|
||||
const data = await this.fetchWithRetry<{ results: Array<{
|
||||
id: string; title: string; summary: string; category_name: string;
|
||||
relevance: string; highlight: string
|
||||
}> }>(
|
||||
`${this.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
|
||||
{ method: 'GET' }
|
||||
)
|
||||
return (data.results || []).map(r => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
categoryName: r.category_name,
|
||||
relevance: r.relevance,
|
||||
highlight: r.highlight,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/health`,
|
||||
{ method: 'GET' },
|
||||
`health-${Date.now()}`
|
||||
)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
setTenantId(tenantId: string): void { this.tenantId = tenantId }
|
||||
getTenantId(): string { return this.tenantId }
|
||||
setProjectId(projectId: string | undefined): void { this.projectId = projectId }
|
||||
getProjectId(): string | undefined { return this.projectId }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user