sozsoft-platform/ui/src/services/setup.service.ts

216 lines
6.4 KiB
TypeScript
Raw Normal View History

2026-04-23 10:36:51 +00:00
import apiService from './api.service'
2026-04-28 17:12:14 +00:00
import { store } from '@/store'
import { applicationConfigurationUrl } from './abpConfig.service'
import { MigrateLogEntry, MigrateLogLevel, SetupStatusDto } from '@/proxy/setup/models'
2026-04-23 10:36:51 +00:00
/**
* Sunucunun veritabanı/kurulum durumu.
*
* Hata durumu her çıran tarafından ayrıca ele alınır (kurulum moduna geçiş kararı,
* durum kartı), bu yüzden global hata diyaloğu bastırılır.
*/
2026-04-23 10:36:51 +00:00
export const getSetupStatus = () =>
apiService.fetchData<SetupStatusDto>({
method: 'GET',
url: '/api/setup/application-status',
silentError: true,
2026-04-23 10:36:51 +00:00
})
const getMigrateUrl = (): string => {
2026-04-23 10:36:51 +00:00
const base = import.meta.env.VITE_API_URL ?? ''
return `${base}/api/setup/migrate`
}
/**
* Sunucu yeniden başlatıldıktan sonra tam ABP stack ile ayağa kalkana kadar
* application-configuration endpoint'ini poll eder.
*
* Arka arkaya iki başarılı yanıt beklenir; tek başarılı yanıt sunucu henüz
* tüm modülleri yüklemeden de dönebilir.
*
* @returns Poll'u iptal eden fonksiyon.
*/
export const pollUntilServerReady = (
onReady: () => void,
onAttempt?: (attempt: number) => void,
): (() => void) => {
const REQUIRED_CONSECUTIVE_SUCCESS = 2
const INITIAL_DELAY_MS = 3000
const SUCCESS_RETRY_MS = 1000
const FAILURE_RETRY_MS = 2000
let attempt = 0
let successCount = 0
let timerId: ReturnType<typeof setTimeout> | null = null
let cancelled = false
const tick = async () => {
if (cancelled) return
attempt++
onAttempt?.(attempt)
try {
const res = await fetch(
`${import.meta.env.VITE_API_URL ?? ''}${applicationConfigurationUrl(false)}`,
{ method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store' },
)
if (res.status === 200) {
const json = await res.json()
if (json && typeof json.currentUser === 'object') {
successCount++
if (successCount >= REQUIRED_CONSECUTIVE_SUCCESS) {
onReady()
return
}
timerId = setTimeout(tick, SUCCESS_RETRY_MS)
return
}
}
} catch {
/* sunucu henüz ayakta değil */
}
successCount = 0
timerId = setTimeout(tick, FAILURE_RETRY_MS)
}
// Sunucunun kapanması için kısa bir bekleme.
timerId = setTimeout(tick, INITIAL_DELAY_MS)
return () => {
cancelled = true
if (timerId) clearTimeout(timerId)
}
}
export interface MigrateStreamOptions {
/**
* Kurulum parolası. Veritabanı yokken (sunucu kurulum modundayken) zorunludur;
* canlı sistemde bunun yerine oturum token'ı ile yetkilendirme yapılır.
*/
setupPassword?: string
signal?: AbortSignal
/** Her log satırı için çağrılır. */
onLog?: (entry: MigrateLogEntry) => void
/** Migration başarıyla tamamlandığında. */
onSuccess?: () => void
/** Sunucu yeniden başlıyor — istemci hazır olana kadar poll etmeli. */
onRestart?: () => void
/** Akış hata ile bitti. */
onError?: (error: Error) => void
/** Başarılı veya başarısız — akış sona erdi. */
onFinished?: () => void
}
/**
* /api/setup/migrate SSE akışının tek istemcisi.
*
* Hem /setup sayfası hem SQL Query Manager hem de Wizard dosya yöneticisi bunu kullanır;
* SSE ayrıştırma mantığı yalnızca burada bulunur.
*/
export const streamSetupMigrate = async (options: MigrateStreamOptions = {}): Promise<void> => {
const { setupPassword, signal, onLog, onSuccess, onRestart, onError, onFinished } = options
// Kurulum parolası kullanılıyorsa oturum token'ı gönderilmez. Sunucu zaten parola
// yolunu seçtiğinde token'a bakmaz; token göndermek yalnızca veritabanına bağımlı
// ara katmanların (oturum doğrulama, dinamik claim'ler) veritabanı düşükken
// devreye girip isteği bloke etmesine yol açar.
const token = setupPassword ? null : store.getState().auth.session.token
let failed = false
const fail = (error: Error) => {
if (failed) return
failed = true
onError?.(error)
}
2026-04-28 17:12:14 +00:00
try {
const response = await fetch(getMigrateUrl(), {
2026-04-28 17:12:14 +00:00
method: 'POST',
headers: {
Accept: 'text/event-stream',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(setupPassword ? { 'X-Setup-Password': setupPassword } : {}),
2026-04-28 17:12:14 +00:00
},
signal,
2026-04-28 17:12:14 +00:00
})
if (!response.ok || !response.body) {
// Sunucu reddetme nedenini gövdede düz metin olarak döner (yanlış parola,
// eksik yapılandırma, eksik izin). Bunu yutmak hatayı teşhis edilemez yapıyordu.
const serverReason = await response.text().catch(() => '')
fail(
new Error(
serverReason.trim() ||
(response.status === 403
? 'Not authorized to run database migration.'
: `Server responded with status ${response.status}`),
),
)
2026-04-28 17:12:14 +00:00
return
}
const reader = response.body.getReader()
2026-04-28 17:12:14 +00:00
const decoder = new TextDecoder()
let buffer = ''
let sawSuccess = false
const handleEntry = (entry: MigrateLogEntry) => {
const level = (entry.level ?? 'info') as MigrateLogLevel
if (level === 'done') {
// Sunucu "done" gönderdi ama öncesinde success/restart gelmediyse akış başarısızdır.
if (!sawSuccess) fail(new Error(entry.message || 'Migration failed.'))
return
}
onLog?.(entry)
if (level === 'success') {
sawSuccess = true
onSuccess?.()
} else if (level === 'restart') {
sawSuccess = true
onRestart?.()
} else if (level === 'error') {
fail(new Error(entry.message || 'Migration failed.'))
}
}
const flushLines = (chunk: string) => {
buffer += chunk
2026-04-28 17:12:14 +00:00
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
2026-04-28 17:12:14 +00:00
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed.startsWith('data:')) continue
const raw = trimmed.slice(5).trim()
if (!raw) continue
try {
handleEntry(JSON.parse(raw) as MigrateLogEntry)
} catch {
onLog?.({ level: 'info', message: raw })
2026-04-28 17:12:14 +00:00
}
}
}
while (true) {
const { done, value } = await reader.read()
if (done) break
flushLines(decoder.decode(value, { stream: true }))
}
2026-04-28 17:12:14 +00:00
} catch (err) {
if ((err as Error)?.name !== 'AbortError') {
fail(err instanceof Error ? err : new Error(String(err)))
}
} finally {
onFinished?.()
}
2026-04-28 17:12:14 +00:00
}