/
man4j
/
consistique
Обзор
Документация
Войти
/
man4j
/
consistique
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/shared/api/client.ts
617 строк
22 KB
Vladimir
fixes
29 июл 2026, 12:30
29 июл 2026, 12:30
61f40a9
Код
Авторство
О чём код?
import type { ApiActivityEvent, ApiBinding, ApiDefinition, ApiDefinitionSource, ApiDefinitionSourceConfiguration, ApiDefinitionSourceDetail, ApiDefinitionSourceFile, ApiDefinitionSourceFilePage, ApiDeployment, ApiDomain, ApiEnvironment, ApiInvariant, ApiInvariantManifest, ApiLogicalSource, ApiRun, ApiRunSample, ApiSource, ApiViolation, DeletionImpact, DryRunResult, EnvironmentId, InboxSummary, RunStaleResult, TelegramNotificationSettings, TelegramNotificationSettingsUpdate, TelegramProxySettingsInput, ValidationResult, } from './contracts'; export * from './contracts'; function apiErrorMessage(detail: unknown) { if (typeof detail === 'string') return detail; if (detail && typeof detail === 'object') { const payload = detail as Record<string, unknown>; const code = typeof payload.error_code === 'string' ? payload.error_code : null; const message = typeof payload.message === 'string' ? payload.message : null; if (code && message) return `${code}: ${message}`; if (code) return code; } return JSON.stringify(detail); } async function request<T>(path: string, init?: RequestInit): Promise<T> { const response = await fetch(path, { ...init, headers: init?.body ? { 'Content-Type': 'application/json', ...init.headers } : init?.headers, }); if (!response.ok) { const payload = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(apiErrorMessage(payload.detail) || `Request failed with status ${response.status}`); } if (response.status === 204) return undefined as T; return response.json() as Promise<T>; } function definitionView( definition: ApiDefinition, deployment: ApiDeployment | undefined, environment: EnvironmentId, ): ApiInvariant { const manifest = { ...definition.manifest, enabled: deployment?.enabled ?? false, retention_period: deployment?.retention_period ?? '30d', } as ApiInvariantManifest; return { manifest, path: definition.path, definition_hash: definition.definition_hash, script_hash: definition.script_hash, current_snapshot_id: definition.current_snapshot_id, registry_status: definition.registry_status, deployment_id: deployment?.deployment_id ?? null, deployment_revision: deployment?.revision ?? null, environment_id: environment, deployed_snapshot_id: deployment?.definition_snapshot_id ?? null, definition_update_policy: deployment?.definition_update_policy ?? 'manual', active_generation: deployment?.active_generation ?? null, pending_generation: deployment?.pending_generation ?? null, last_promoted_at: deployment?.last_promoted_at ?? null, last_promoted_via: deployment?.last_promoted_via ?? null, update_available: deployment?.update_available ?? false, script_update_available: deployment?.script_update_available ?? false, rebaseline_pending: deployment?.rebaseline_pending ?? false, not_ready_reason: deployment?.not_ready_reason ?? null, runner_available: deployment?.runner_available ?? definition.runner_available, summary_status: deployment?.summary_status ?? 'NOT_DEPLOYED', runnable_since: deployment?.runnable_since ?? null, last_run_id: deployment?.last_run_id ?? null, last_run_status: deployment?.last_run_status ?? null, last_run_error: deployment?.last_run_error ?? null, last_successful_run_id: deployment?.last_successful_run_id ?? null, last_successful_reconciliation_at: deployment?.last_successful_reconciliation_at ?? null, is_stale: deployment?.is_stale ?? false, stale_since: deployment?.stale_since ?? null, source_status: deployment?.last_run_status === 'success' ? 'CONNECTED' : null, active_total: deployment?.active_total ?? 0, candidate_total: deployment?.candidate_total ?? 0, accepted_total: deployment?.accepted_total ?? 0, effective_policy: { ...definition.defaults, reconciliation_interval: deployment?.reconciliation_interval ?? definition.defaults.reconciliation_interval, grace_period: deployment?.grace_period ?? definition.defaults.grace_period, }, management: definition.management, system: deployment?.system ?? definition.system, }; } export async function loadCatalog(environment: EnvironmentId = 'local') { const [definitions, domains, sources, deployments, bindings, sourceAliases] = await Promise.all([ request<ApiDefinition[]>('/api/definitions'), loadDomains(), request<ApiSource[]>('/api/sources'), request<ApiDeployment[]>(`/api/deployments?environment_id=${environment}`), request<ApiBinding[]>(`/api/source-bindings?environment_id=${environment}`), request<ApiLogicalSource[]>('/api/source-aliases'), ]); const deploymentByDefinition = new Map(deployments.map((item) => [item.invariant_id, item])); return { definitions, domains, sources, deployments, bindings, sourceAliases, invariants: definitions.map((definition) => definitionView(definition, deploymentByDefinition.get(definition.invariant_id), environment), ), }; } export function loadDomains() { return request<ApiDomain[]>('/api/domains'); } export function loadEnvironments() { return request<ApiEnvironment[]>('/api/environments'); } export function createEnvironment(displayName: string) { return request<ApiEnvironment>('/api/environments', { method: 'POST', body: JSON.stringify({ display_name: displayName }), }); } export function updateEnvironment(environmentId: string, displayName: string) { return request<ApiEnvironment>(`/api/environments/${encodeURIComponent(environmentId)}`, { method: 'PATCH', body: JSON.stringify({ display_name: displayName }), }); } export function getEnvironmentDeletionImpact(environmentId: string) { return request<DeletionImpact>( `/api/environments/${encodeURIComponent(environmentId)}/deletion-impact`, ); } export function deleteEnvironment(environmentId: string) { return request<void>(`/api/environments/${encodeURIComponent(environmentId)}`, { method: 'DELETE', }); } export function loadSourceAliases() { return request<ApiLogicalSource[]>('/api/source-aliases'); } export function createSourceAlias(payload: { display_name: string; description: string; required_type: ApiLogicalSource['required_type']; }) { return request<ApiLogicalSource>('/api/source-aliases', { method: 'POST', body: JSON.stringify(payload), }); } export function updateSourceAlias(sourceAlias: string, payload: Record<string, unknown>) { return request<ApiLogicalSource>(`/api/source-aliases/${encodeURIComponent(sourceAlias)}`, { method: 'PATCH', body: JSON.stringify(payload), }); } export function getSourceAliasDeletionImpact(sourceAlias: string) { return request<DeletionImpact>( `/api/source-aliases/${encodeURIComponent(sourceAlias)}/deletion-impact`, ); } export function deleteSourceAlias(sourceAlias: string) { return request<void>(`/api/source-aliases/${encodeURIComponent(sourceAlias)}`, { method: 'DELETE', }); } export function createDomain(payload: { title: string; reconciliation_interval: string; grace_period: string }) { return request('/api/domains/drafts', { method: 'POST', body: JSON.stringify(payload) }); } export function updateDomain(domainId: string, payload: Record<string, unknown>) { return request(`/api/domains/${encodeURIComponent(domainId)}`, { method: 'PATCH', body: JSON.stringify(payload) }); } export function getDomainDeletionImpact(domainId: string) { return request<DeletionImpact>(`/api/domains/${encodeURIComponent(domainId)}/deletion-impact`); } export function deleteDomain(domainId: string, cascade = false) { return request<void>(`/api/domains/${encodeURIComponent(domainId)}${cascade ? '?cascade=true' : ''}`, { method: 'DELETE' }); } export function createInvariant(payload: { domain_id: string; title: string; description: string; source_id: string; grace_period?: string | null; count_sql: string; items_sql: string; }) { const { source_id, ...definition } = payload; return request<ApiDefinition>('/api/definitions', { method: 'POST', body: JSON.stringify({ ...definition, source_alias: source_id }), }); } export function updateInvariant(invariantId: string, payload: Record<string, unknown>) { const normalized = { ...payload }; if ('source_id' in normalized) { normalized.source_alias = normalized.source_id; delete normalized.source_id; } return request<ApiDefinition>(`/api/definitions/${encodeURIComponent(invariantId)}`, { method: 'PATCH', body: JSON.stringify(normalized), }); } export function deleteInvariant(invariantId: string, cascade = false) { return request<void>(`/api/definitions/${encodeURIComponent(invariantId)}${cascade ? '?cascade=true' : ''}`, { method: 'DELETE' }); } export function createSource(payload: { display_name: string; type: ApiSource['type']; environment: ApiSource['environment']; username: string; password: string; host?: string; port?: number; database_name?: string; statement_timeout_ms: number; }) { return request<ApiSource>('/api/sources', { method: 'POST', body: JSON.stringify(payload) }); } export function checkSourceConnection(payload: { source_id?: string; host: string; port: number; database_name: string; username?: string; password?: string }) { return request<{ ok: boolean; latency_ms: number; error_message: string | null }>('/api/sources/connection-check', { method: 'POST', body: JSON.stringify(payload) }); } export function loadTelegramNotificationSettings() { return request<TelegramNotificationSettings>('/api/settings/notifications/telegram'); } export function updateTelegramNotificationSettings( payload: TelegramNotificationSettingsUpdate, ) { return request<TelegramNotificationSettings>('/api/settings/notifications/telegram', { method: 'PUT', body: JSON.stringify(payload), }); } export function checkTelegramConnection( payload: TelegramProxySettingsInput & { bot_token?: string }, ) { return request<{ ok: boolean; bot_id: number; username: string | null }>( '/api/settings/notifications/telegram/check', { method: 'POST', body: JSON.stringify(payload), }, ); } export function sendTelegramTestMessage(payload: { bot_token?: string; chat_id: string | null; message_thread_id: number | null; } & TelegramProxySettingsInput) { return request<{ ok: boolean; message_id: number }>( '/api/settings/notifications/telegram/test', { method: 'POST', body: JSON.stringify(payload), }, ); } export function updateSource(sourceId: string, payload: Record<string, unknown>) { return request<ApiSource>(`/api/sources/${encodeURIComponent(sourceId)}`, { method: 'PATCH', body: JSON.stringify(payload) }); } export function getSourceDeletionImpact(sourceId: string) { return request<DeletionImpact>(`/api/sources/${encodeURIComponent(sourceId)}/deletion-impact`); } export function deleteSource(sourceId: string, cascade = false) { return request<void>(`/api/sources/${encodeURIComponent(sourceId)}${cascade ? '?cascade=true' : ''}`, { method: 'DELETE' }); } export function loadDefinitionSources() { return request<ApiDefinitionSource[]>('/api/definition-sources'); } export function loadDefinitionSourceConfiguration() { return request<ApiDefinitionSourceConfiguration>('/api/definition-sources/configuration'); } export function loadDefinitionSource( sourceId: string, options: { fileLimit?: number; fileOffset?: number; scanLimit?: number } = {}, ) { const query = new URLSearchParams({ file_limit: String(options.fileLimit ?? 200), file_offset: String(options.fileOffset ?? 0), scan_limit: String(options.scanLimit ?? 20), }); return request<ApiDefinitionSourceDetail>( `/api/definition-sources/${encodeURIComponent(sourceId)}?${query}`, ); } export function loadDefinitionSourceFiles(options: { sourceId?: string; status?: ApiDefinitionSourceFile['status']; includeDetached?: boolean; limit?: number; offset?: number; } = {}) { const query = new URLSearchParams({ limit: String(options.limit ?? 200), offset: String(options.offset ?? 0), }); if (options.sourceId) query.set('source_id', options.sourceId); if (options.status) query.set('status', options.status); if (options.includeDetached !== undefined) { query.set('include_detached', String(options.includeDetached)); } return request<ApiDefinitionSourceFilePage>(`/api/definition-sources/files?${query}`); } export async function loadDefinitionSourceFileContent( sourceId: string, relativePath: string, ) { const query = new URLSearchParams({ relative_path: relativePath }); const response = await fetch( `/api/definition-sources/${encodeURIComponent(sourceId)}/files/content?${query}`, ); if (!response.ok) { const payload = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(apiErrorMessage(payload.detail) || 'Unable to load definition source file'); } return response.text(); } export function createDefinitionSource(payload: { name: string; directory_path: string; enabled: boolean; }) { return request<ApiDefinitionSourceDetail>('/api/definition-sources', { method: 'POST', body: JSON.stringify(payload), }); } export function updateDefinitionSource( sourceId: string, payload: Partial<Pick<ApiDefinitionSource, 'name' | 'directory_path' | 'enabled'>>, ) { return request<ApiDefinitionSourceDetail>( `/api/definition-sources/${encodeURIComponent(sourceId)}`, { method: 'PATCH', body: JSON.stringify(payload) }, ); } export function syncDefinitionSource(sourceId: string) { return request<ApiDefinitionSourceDetail>( `/api/definition-sources/${encodeURIComponent(sourceId)}/sync`, { method: 'POST' }, ); } export function publishDefinitionSourceFile( sourceId: string, relativePath: string, expectedObservedFileHash: string, ) { return request<ApiDefinitionSourceFile>( `/api/definition-sources/${encodeURIComponent(sourceId)}/publish`, { method: 'POST', body: JSON.stringify({ relative_path: relativePath, expected_observed_file_hash: expectedObservedFileHash, actor: 'ui', }), }, ); } export function detachDefinitionSourceFile( sourceId: string, relativePath: string, reason: string, ) { return request<ApiDefinitionSourceFile>( `/api/definition-sources/${encodeURIComponent(sourceId)}/detach`, { method: 'POST', body: JSON.stringify({ relative_path: relativePath, confirm: true, reason, actor: 'ui', }), }, ); } export function forgetDefinitionSourceFile(sourceId: string, relativePath: string) { return request<void>( `/api/definition-sources/${encodeURIComponent(sourceId)}/forget`, { method: 'POST', body: JSON.stringify({ relative_path: relativePath, confirm: true, }), }, ); } export function deleteDefinitionSource(sourceId: string, reason: string) { const query = new URLSearchParams({ confirm: 'true', reason }); return request<void>( `/api/definition-sources/${encodeURIComponent(sourceId)}?${query}`, { method: 'DELETE' }, ); } export function bindSource(environment: EnvironmentId, alias: string, sourceId: string) { return request<ApiBinding>(`/api/environments/${environment}/source-bindings/${encodeURIComponent(alias)}`, { method: 'PUT', body: JSON.stringify({ source_id: sourceId }), }); } export function deleteBinding(environment: EnvironmentId, alias: string) { return request<void>(`/api/environments/${environment}/source-bindings/${encodeURIComponent(alias)}`, { method: 'DELETE' }); } export function createDeployment(payload: { invariant_id: string; environment_id: EnvironmentId; enabled?: boolean; reconciliation_interval: string; grace_period: string; retention_period?: string; }) { return request<ApiDeployment>('/api/deployments', { method: 'POST', body: JSON.stringify(payload) }); } export function updateDeployment(deploymentId: string, expectedRevision: number, payload: Record<string, unknown>) { return request<ApiDeployment>(`/api/deployments/${deploymentId}`, { method: 'PATCH', body: JSON.stringify({ expected_revision: expectedRevision, ...payload }), }); } export function deleteDeployment(deploymentId: string, expectedRevision: number) { return request<void>(`/api/deployments/${deploymentId}?expected_revision=${expectedRevision}`, { method: 'DELETE' }); } export function promoteDeployment(deploymentId: string, snapshotId: string, expectedRevision: number) { return request<ApiDeployment>(`/api/deployments/${deploymentId}/promote`, { method: 'POST', body: JSON.stringify({ definition_snapshot_id: snapshotId, expected_revision: expectedRevision }), }); } export function validateInvariant(payload: { manifest: Record<string, unknown>; count_sql: string; items_sql: string }) { return request<ValidationResult>('/api/definitions/validate', { method: 'POST', body: JSON.stringify(payload) }); } export async function loadInvariantSql(invariantId: string) { const encoded = encodeURIComponent(invariantId); const [countResponse, itemsResponse] = await Promise.all([ fetch(`/api/invariants/${encoded}/artifacts/count.sql`), fetch(`/api/invariants/${encoded}/artifacts/items.sql`), ]); if (!countResponse.ok || !itemsResponse.ok) throw new Error('Unable to load invariant SQL files'); return { countSql: await countResponse.text(), itemsSql: await itemsResponse.text() }; } export function loadArtifact(invariantId: string, artifact: string) { return fetch(`/api/invariants/${encodeURIComponent(invariantId)}/artifacts/${artifact}`).then(async (response) => { if (!response.ok) throw new Error(`Unable to load ${artifact}`); return response.text(); }); } export function loadInboxSummary(window: '1h' | '24h' | '7d', environment: EnvironmentId) { return request<InboxSummary>(`/api/inbox/summary?environment_id=${environment}&window=${window}`); } export function loadViolations(filters: { status?: ApiViolation['lifecycle_status']; returned?: boolean; scope?: 'current' | 'all'; deploymentId?: string; limit?: number } = {}) { if (!filters.deploymentId) return Promise.resolve([] as ApiViolation[]); const query = new URLSearchParams({ deployment_id: filters.deploymentId }); if (filters.status) query.set('status', filters.status); if (filters.returned !== undefined) query.set('returned', String(filters.returned)); if (filters.scope) query.set('scope', filters.scope); if (filters.limit) query.set('limit', String(filters.limit)); return request<ApiViolation[]>(`/api/violations?${query}`); } export function loadRuns(limit = 100, environment: EnvironmentId = 'local') { return request<ApiRun[]>(`/api/runs?environment_id=${environment}&limit=${limit}`); } export function loadInvariantRuns(deploymentId: string, limit = 20) { return request<ApiRun[]>(`/api/deployments/${deploymentId}/runs?limit=${limit}`); } export function loadDeploymentActivity( deploymentId: string, window: '1h' | '24h' | '7d', eventType: ApiActivityEvent['event_type'], limit = 200, ) { const query = new URLSearchParams({ window, event_type: eventType, limit: String(limit) }); return request<ApiActivityEvent[]>( `/api/deployments/${encodeURIComponent(deploymentId)}/activity?${query}`, ); } export function loadRun(runId: string) { return request<ApiRun>(`/api/runs/${encodeURIComponent(runId)}`); } export async function waitForRun(runId: string, delayMs = 1000) { while (true) { const run = await loadRun(runId); if (run.status !== 'running') return run; await new Promise((resolve) => window.setTimeout(resolve, delayMs)); } } export function loadRunSamples(runId: string, limit = 100) { return request<ApiRunSample[]>(`/api/runs/${encodeURIComponent(runId)}/violations?limit=${limit}`); } export function runInvariant(deploymentId: string) { return request<{ run_id: string; status: ApiRun['status']; existing: boolean }>(`/api/deployments/${deploymentId}/run`, { method: 'POST', body: JSON.stringify({ idempotency_key: crypto.randomUUID() }), }); } export function runStaleInvariants(environment: EnvironmentId) { return request<RunStaleResult>( `/api/inbox/run-stale?environment_id=${encodeURIComponent(environment)}`, { method: 'POST' }, ); } export function publishInvariant(deploymentId: string, revision: number) { return updateDeployment(deploymentId, revision, { enabled: true }); } export function disableInvariant(deploymentId: string, revision: number) { return updateDeployment(deploymentId, revision, { enabled: false }); } export function acceptCurrentBacklog(deploymentId: string, targetRunId: string, reason: string, comment: string | null) { return request<Record<string, unknown>>(`/api/deployments/${deploymentId}/accept-current-backlog`, { method: 'POST', body: JSON.stringify({ target_run_id: targetRunId, accepted_by: 'ui', reason, comment, idempotency_key: crypto.randomUUID(), confirm: true, }), }); } export function unacceptBacklog(deploymentId: string, acceptanceBatchId: string) { return request<Record<string, unknown>>(`/api/deployments/${deploymentId}/unaccept-backlog`, { method: 'POST', body: JSON.stringify({ acceptance_batch_id: acceptanceBatchId, confirm: true }), }); } export function dryRunInvariant(payload: { manifest: Record<string, unknown>; count_sql: string; items_sql: string }) { return request<DryRunResult>('/api/definitions/dry-run', { method: 'POST', body: JSON.stringify(payload) }); } export function dryRunRegisteredInvariant(invariantId: string) { return request<DryRunResult>(`/api/invariants/${encodeURIComponent(invariantId)}/dry-run`, { method: 'POST' }); }