| 1 | import type { createClient } from '@libsql/client' |
| 2 | import type { DesignContract } from '@shared/generation' |
| 3 | import { createDefaultDesignContract } from '../../presentation/design-contract' |
| 4 | |
| 5 | type LibSqlClient = ReturnType<typeof createClient> |
| 6 | |
| 7 | const FALLBACK_TITLE_FONT = 'Inter' |
| 8 | const FALLBACK_BODY_FONT = 'Noto Sans SC' |
| 9 | |
| 10 | const getRowValue = (row: unknown, key: string): unknown => { |
| 11 | if (row && typeof row === 'object' && !Array.isArray(row) && key in row) { |
| 12 | return (row as Record<string, unknown>)[key] |
| 13 | } |
| 14 | return undefined |
| 15 | } |
| 16 | |
| 17 | const parseDesignContract = (value: unknown): DesignContract | null => { |
| 18 | if (typeof value !== 'string' || value.trim().length === 0) return null |
| 19 | try { |
| 20 | const parsed = JSON.parse(value) as unknown |
| 21 | if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null |
| 22 | return parsed as DesignContract |
| 23 | } catch { |
| 24 | return null |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | const normalizeFont = (value: unknown): string => String(value ?? '').replace(/\s+/g, ' ').trim() |
| 29 | |
| 30 | const withFontFallback = (contract: DesignContract): DesignContract => { |
| 31 | const titleFont = normalizeFont(contract.titleFont) || FALLBACK_TITLE_FONT |
| 32 | const bodyFont = normalizeFont(contract.bodyFont) || FALLBACK_BODY_FONT |
| 33 | if (titleFont === contract.titleFont && bodyFont === contract.bodyFont) return contract |
| 34 | return { ...contract, titleFont, bodyFont } |
| 35 | } |
| 36 | |
| 37 | export const patchDesignContractFonts = async (client: LibSqlClient): Promise<void> => { |
| 38 | await client.execute({ |
| 39 | sql: ` |
| 40 | UPDATE sessions |
| 41 | SET design_contract = ?, updated_at = ? |
| 42 | WHERE provider = 'import' |
| 43 | AND model IN ('session-file-import', 'pptx-import') |
| 44 | AND (design_contract IS NULL OR TRIM(design_contract) = '') |
| 45 | `, |
| 46 | args: [JSON.stringify(createDefaultDesignContract()), Math.floor(Date.now() / 1000)] |
| 47 | }) |
| 48 | |
| 49 | const result = await client.execute(` |
| 50 | SELECT id, design_contract |
| 51 | FROM sessions |
| 52 | WHERE design_contract IS NOT NULL |
| 53 | AND TRIM(design_contract) <> '' |
| 54 | `) |
| 55 | |
| 56 | for (const row of result.rows || []) { |
| 57 | const sessionId = String(getRowValue(row, 'id') || '').trim() |
| 58 | const rawContract = getRowValue(row, 'design_contract') |
| 59 | if (!sessionId || typeof rawContract !== 'string') continue |
| 60 | |
| 61 | const contract = parseDesignContract(rawContract) |
| 62 | if (!contract) continue |
| 63 | |
| 64 | const patched = withFontFallback(contract) |
| 65 | if (patched.titleFont === contract.titleFont && patched.bodyFont === contract.bodyFont) { |
| 66 | continue |
| 67 | } |
| 68 | |
| 69 | await client.execute({ |
| 70 | sql: 'UPDATE sessions SET design_contract = ? WHERE id = ? AND design_contract = ?', |
| 71 | args: [JSON.stringify(patched), sessionId, rawContract] |
| 72 | }) |
| 73 | } |
| 74 | } |
| 75 |