import { NextRequest, NextResponse } from 'next/server'; import { requireAdmin } from '@/lib/admin-auth'; import { localStorage } from '@/lib/local-storage'; import { encryptSecret, previewSecret } from '@/lib/server-crypto'; import { getDbClient } from '@/storage/database/local-db'; interface ImportMeta { version: string; platform: string; exported_at: string; tables: string[]; counts: Record; } interface ImportPayload { _meta: ImportMeta; data: Record; options?: { skipAuth?: boolean; }; } const MAX_ROWS_PER_TABLE = 5000; const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UUID_ID_TABLES = new Set([ 'auth.users', 'profiles', 'announcements', 'works', 'credit_transactions', 'orders', 'user_api_keys', 'system_api_configs', 'work_likes', ]); const TABLE_COLUMNS: Record = { profiles: ['id', 'email', 'nickname', 'avatar_url', 'phone', 'role', 'membership_tier', 'membership_expires_at', 'credits_balance', 'daily_quota_used', 'daily_quota_limit', 'is_active', 'preferred_theme', 'created_at', 'updated_at'], site_config: ['id', 'site_name', 'site_tab_title', 'site_description', 'site_keywords', 'logo_url', 'favicon_url', 'announcement', 'membership_enabled', 'terms_of_service', 'privacy_policy', 'about_us', 'help_center', 'filing_info', 'filing_url', 'public_security_filing_info', 'public_security_filing_url', 'updated_at'], site_stats: ['id', 'total_visits', 'total_users', 'total_generations', 'updated_at'], announcements: ['id', 'title', 'content', 'type', 'is_active', 'starts_at', 'expires_at', 'created_at', 'updated_at'], works: ['id', 'user_id', 'title', 'type', 'prompt', 'negative_prompt', 'params', 'result_url', 'thumbnail_url', 'width', 'height', 'duration', 'status', 'is_public', 'likes_count', 'views_count', 'created_at', 'updated_at'], credit_transactions: ['id', 'user_id', 'amount', 'balance_after', 'type', 'description', 'related_work_id', 'created_at'], orders: ['id', 'user_id', 'order_no', 'product_type', 'product_name', 'amount', 'credits_amount', 'status', 'payment_method', 'paid_at', 'created_at', 'updated_at'], user_api_keys: ['id', 'user_id', 'provider', 'supplier_name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'is_active', 'created_at', 'updated_at'], system_api_configs: ['id', 'provider', 'name', 'api_url', 'model_name', 'note', 'api_key_encrypted', 'api_key_preview', 'type', 'credits_per_use', 'is_active', 'sort_order', 'created_at', 'updated_at'], payment_methods: ['id', 'type', 'name', 'is_active', 'public_config', 'secret_config_encrypted', 'secret_config_preview', 'created_at', 'updated_at'], work_likes: ['id', 'user_id', 'work_id', 'created_at'], }; const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'; const AUTH_USER_COLUMNS = ['id', 'email', 'created_at', 'raw_user_meta_data', 'password_hash']; const CONFLICT_COLUMNS: Record = { 'auth.users': ['id'], profiles: ['id'], site_config: ['id'], site_stats: ['id'], announcements: ['id'], works: ['id'], credit_transactions: ['id'], orders: ['id'], user_api_keys: ['id'], system_api_configs: ['id'], payment_methods: ['id'], work_likes: ['id'], }; type ImportResult = { imported: number; skipped: number; errors: string[] }; type ImportContext = { userIdMap: Map; workIdMap: Map; emailUserIdMap: Map; apiKeyIdMap: Map; apiKeyOwnerIdMap: Map; columnCache: Map>; defaultableColumnCache: Map>; }; export async function POST(request: NextRequest) { const authError = await requireAdmin(request); if (authError) return authError; try { const body: ImportPayload = await request.json(); const { _meta, data } = body; const skipAuth = body.options?.skipAuth === true; if (!_meta || _meta.platform !== 'miaojing' || !data || typeof data !== 'object') { return NextResponse.json({ error: '无效的导入文件:格式不匹配' }, { status: 400 }); } const client = await getDbClient(); const result: Record = {}; try { const context = await buildImportContext(client, data); if (!skipAuth && Array.isArray(data.auth_users)) { result.auth_users = await importRows(client, 'auth.users', AUTH_USER_COLUMNS, data.auth_users, context); } else { result.auth_users = { imported: 0, skipped: Array.isArray(data.auth_users) ? data.auth_users.length : 0, errors: skipAuth ? ['已按选项跳过认证账号导入'] : [], }; } for (const [table, allowedColumns] of Object.entries(TABLE_COLUMNS)) { const rows = data[table]; result[table] = await importRows(client, table, allowedColumns, Array.isArray(rows) ? rows : [], context); } return NextResponse.json({ success: true, message: '数据导入完成', details: result, meta: _meta }); } finally { client.release(); } } catch (err) { console.error('[data-import] Error:', err instanceof Error ? err.message : err); return NextResponse.json({ error: err instanceof Error ? err.message : '导入失败' }, { status: 500 }); } } async function importRows( client: Awaited>, table: string, allowedColumns: string[], rows: unknown[], context: ImportContext, ): Promise { if (rows.length > MAX_ROWS_PER_TABLE) { return { imported: 0, skipped: rows.length, errors: [`${table}: 单表最多允许导入 ${MAX_ROWS_PER_TABLE} 行`] }; } let imported = 0; let skipped = 0; const errors: string[] = []; const existingColumns = await getExistingColumns(client, table, context); const defaultableColumns = await getDefaultableColumns(client, table, context); const effectiveAllowedColumns = allowedColumns.filter(col => existingColumns.has(col)); for (const rawRow of rows) { const row = await normalizeImportRow(table, rawRow as Record, context); const cols = Object.keys(row).filter(col => ( effectiveAllowedColumns.includes(col) && !(row[col] == null && defaultableColumns.has(col)) )); if (!cols.includes('id') || cols.length === 0) { skipped++; errors.push(`${table}: 缺少 id 或没有允许导入的字段`); continue; } try { const vals = cols.map(col => row[col]); const placeholders = cols.map((_, i) => `$${i + 1}`).join(', '); const conflictCols = CONFLICT_COLUMNS[table] || ['id']; const mergeAssignments = getMergeAssignments(table, cols); const conflictAction = mergeAssignments.length > 0 ? `DO UPDATE SET ${mergeAssignments.join(', ')}` : 'DO NOTHING'; const insertResult = await client.query( `INSERT INTO ${table} AS target (${cols.join(', ')}) VALUES (${placeholders}) ON CONFLICT (${conflictCols.join(', ')}) ${conflictAction}`, vals, ); if ((insertResult.rowCount || 0) > 0) { imported++; } else { skipped++; } } catch (e) { skipped++; errors.push(`${table}: ${e instanceof Error ? e.message : 'unknown error'}`); } } return { imported, skipped, errors }; } async function buildImportContext( client: Awaited>, data: Record, ): Promise { const userIdMap = new Map(); const workIdMap = new Map(); const emailUserIdMap = new Map(); const apiKeyIdMap = new Map(); const apiKeyOwnerIdMap = new Map(); const profileRows = Array.isArray(data.profiles) ? data.profiles : []; const authRows = Array.isArray(data.auth_users) ? data.auth_users : []; const profileEmails = new Map(); for (const raw of profileRows) { const row = raw as Record; seedUuidMap(userIdMap, row.id); if (typeof row.id === 'string' && typeof row.email === 'string' && row.email.trim()) { const email = row.email.trim().toLowerCase(); profileEmails.set(email, row.id); emailUserIdMap.set(email, userIdMap.get(row.id) || row.id); } } for (const raw of authRows) { const row = raw as Record; seedUuidMap(userIdMap, row.id); if (typeof row.id === 'string' && typeof row.email === 'string' && row.email.trim() && !profileEmails.has(row.email.trim().toLowerCase())) { const email = row.email.trim().toLowerCase(); profileEmails.set(email, row.id); emailUserIdMap.set(email, userIdMap.get(row.id) || row.id); } } if (profileEmails.size > 0) { const emails = [...profileEmails.keys()]; const existing = await client.query( 'SELECT id, lower(email) AS email FROM profiles WHERE lower(email) = ANY($1)', [emails], ); for (const row of existing.rows) { const importedId = profileEmails.get(row.email); if (importedId && importedId !== row.id) { userIdMap.set(importedId, row.id); emailUserIdMap.set(row.email, row.id); } } } for (const [email, importedId] of profileEmails.entries()) { emailUserIdMap.set(email, userIdMap.get(importedId) || importedId); } const apiKeyRows = Array.isArray(data.user_api_keys) ? data.user_api_keys : []; for (const raw of apiKeyRows) { const row = raw as Record; const oldId = typeof row.id === 'string' && row.id.trim() ? row.id.trim() : ''; if (oldId) { apiKeyIdMap.set(oldId, isUuid(oldId) ? oldId : crypto.randomUUID()); } const ownerId = findImportedWorkUserId(row); const ownerByEmail = findUserIdByEmail(row, { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map(), defaultableColumnCache: new Map(), }); const mappedOwnerId = ownerId ? (userIdMap.get(ownerId) || ownerId) : ownerByEmail; if (oldId && mappedOwnerId) { apiKeyOwnerIdMap.set(oldId, mappedOwnerId); } } const works = Array.isArray(data.works) ? data.works : []; const workUrls = new Map(); for (const raw of works) { const row = raw as Record; seedUuidMap(workIdMap, row.id); if (typeof row.id === 'string' && typeof row.result_url === 'string' && row.result_url.trim() && !isDataUrl(row.result_url)) { workUrls.set(row.result_url.trim(), row.id); } } if (workUrls.size > 0) { const existing = await client.query( 'SELECT id, result_url FROM works WHERE result_url = ANY($1)', [[...workUrls.keys()]], ); for (const row of existing.rows) { const importedId = workUrls.get(row.result_url); if (importedId && importedId !== row.id) { workIdMap.set(importedId, row.id); } } } return { userIdMap, workIdMap, emailUserIdMap, apiKeyIdMap, apiKeyOwnerIdMap, columnCache: new Map(), defaultableColumnCache: new Map(), }; } async function normalizeImportRow(table: string, row: Record, context: ImportContext): Promise> { const next = { ...row }; if (typeof next.user_id === 'string' && context.userIdMap.has(next.user_id)) { next.user_id = context.userIdMap.get(next.user_id); } if ((!next.user_id || next.user_id === SYSTEM_USER_ID) && findUserIdByEmail(next, context)) { next.user_id = findUserIdByEmail(next, context); } if (typeof next.related_work_id === 'string' && context.workIdMap.has(next.related_work_id)) { next.related_work_id = context.workIdMap.get(next.related_work_id); } if (typeof next.work_id === 'string' && context.workIdMap.has(next.work_id)) { next.work_id = context.workIdMap.get(next.work_id); } if (table === 'auth.users' || table === 'profiles') { const currentId = typeof next.id === 'string' ? next.id : ''; if (currentId && context.userIdMap.has(currentId)) { next.id = context.userIdMap.get(currentId); } } if (table === 'user_api_keys') { const currentId = typeof next.id === 'string' ? next.id : ''; if (currentId && context.apiKeyIdMap.has(currentId)) { next.id = context.apiKeyIdMap.get(currentId); } const importedUserId = findImportedWorkUserId(next); const emailUserId = findUserIdByEmail(next, context); if (importedUserId || emailUserId) { next.user_id = importedUserId ? (context.userIdMap.get(importedUserId) || importedUserId) : emailUserId; } } if (table === 'works') { const currentId = typeof next.id === 'string' ? next.id : ''; if (currentId && context.workIdMap.has(currentId)) { next.id = context.workIdMap.get(currentId); } const importedUserId = findImportedWorkUserId(next) || findUserIdByEmail(next, context) || findUserIdByCustomModel(next, context); if (importedUserId) { next.user_id = context.userIdMap.get(importedUserId) || importedUserId; } if (typeof next.result_url === 'string') { next.result_url = await persistImportMedia(next.result_url, getWorkMediaFolder(next.type, 'results')); } if (typeof next.thumbnail_url === 'string') { next.thumbnail_url = await persistImportMedia(next.thumbnail_url, 'imported/works/thumbnails'); } if (next.params && typeof next.params === 'object') { next.params = await sanitizeImportMedia(next.params, 'imported/works/references'); remapCustomModelId(next.params as Record, context); if ((!next.user_id || next.user_id === SYSTEM_USER_ID) && findUserIdByCustomModel(next, context)) { next.user_id = findUserIdByCustomModel(next, context); } } } if (table === 'user_api_keys') { if (typeof next.note !== 'string' || next.note.trim() === '') { next.note = '导入的 API Key'; } if (typeof next.type !== 'string' || next.type.trim() === '') { next.type = 'image'; } const rawEncrypted = typeof next.api_key_encrypted === 'string' ? next.api_key_encrypted.trim() : ''; const rawApiKey = typeof next.apiKey === 'string' ? next.apiKey.trim() : ''; const secret = rawApiKey || rawEncrypted; if (secret) { next.api_key_encrypted = encryptSecret(secret); next.api_key_preview = typeof next.api_key_preview === 'string' && next.api_key_preview ? next.api_key_preview : previewSecret(secret); } } if (UUID_ID_TABLES.has(table)) { const currentId = typeof next.id === 'string' ? next.id : ''; if (!isUuid(currentId)) { next.id = crypto.randomUUID(); } } return next; } function findImportedWorkUserId(row: Record): string | null { const directKeys = ['user_id', 'userId', 'publisher_id', 'publisherId', 'owner_id', 'ownerId', 'created_by', 'createdBy']; for (const key of directKeys) { const value = row[key]; if (typeof value === 'string' && value.trim() && value !== 'anonymous' && value !== '00000000-0000-0000-0000-000000000000') { return value.trim(); } } const params = row.params && typeof row.params === 'object' ? row.params as Record : null; if (!params) return null; for (const key of directKeys) { const value = params[key]; if (typeof value === 'string' && value.trim() && value !== 'anonymous' && value !== '00000000-0000-0000-0000-000000000000') { return value.trim(); } } return null; } function findUserIdByEmail(row: Record, context: ImportContext): string | null { const directKeys = ['email', 'user_email', 'userEmail', 'publisher_email', 'publisherEmail', 'owner_email', 'ownerEmail']; for (const key of directKeys) { const value = row[key]; if (typeof value === 'string' && value.trim()) { const mapped = context.emailUserIdMap.get(value.trim().toLowerCase()); if (mapped) return mapped; } } const params = row.params && typeof row.params === 'object' ? row.params as Record : null; if (!params) return null; for (const key of directKeys) { const value = params[key]; if (typeof value === 'string' && value.trim()) { const mapped = context.emailUserIdMap.get(value.trim().toLowerCase()); if (mapped) return mapped; } } return null; } function findUserIdByCustomModel(row: Record, context: ImportContext): string | null { const params = row.params && typeof row.params === 'object' ? row.params as Record : null; const model = typeof params?.model === 'string' ? params.model : typeof row.model === 'string' ? row.model : ''; if (!model.startsWith('custom:')) return null; const oldId = model.slice('custom:'.length); return context.apiKeyOwnerIdMap.get(oldId) || null; } function remapCustomModelId(params: Record, context: ImportContext): void { const model = typeof params.model === 'string' ? params.model : ''; if (!model.startsWith('custom:')) return; const oldId = model.slice('custom:'.length); const newId = context.apiKeyIdMap.get(oldId); if (newId) { params.model = `custom:${newId}`; } } function getMergeAssignments(table: string, cols: string[]): string[] { const has = (column: string) => cols.includes(column); const assignments: string[] = []; if (table === 'auth.users') { if (has('email')) assignments.push(`email = COALESCE(NULLIF(target.email, ''), EXCLUDED.email)`); if (has('raw_user_meta_data')) assignments.push(`raw_user_meta_data = COALESCE(target.raw_user_meta_data, EXCLUDED.raw_user_meta_data)`); if (has('password_hash')) assignments.push(`password_hash = COALESCE(NULLIF(target.password_hash, ''), EXCLUDED.password_hash)`); return assignments; } if (table === 'profiles') { if (has('email')) assignments.push(`email = COALESCE(NULLIF(target.email, ''), EXCLUDED.email)`); if (has('nickname')) assignments.push(`nickname = COALESCE(NULLIF(target.nickname, ''), EXCLUDED.nickname)`); if (has('avatar_url')) assignments.push(`avatar_url = COALESCE(NULLIF(target.avatar_url, ''), EXCLUDED.avatar_url)`); if (has('phone')) assignments.push(`phone = COALESCE(NULLIF(target.phone, ''), EXCLUDED.phone)`); if (has('role')) assignments.push(`role = CASE WHEN target.role = 'admin' THEN target.role ELSE COALESCE(NULLIF(target.role, ''), EXCLUDED.role) END`); if (has('membership_tier')) assignments.push(`membership_tier = COALESCE(NULLIF(target.membership_tier, ''), EXCLUDED.membership_tier)`); if (has('membership_expires_at')) assignments.push(`membership_expires_at = COALESCE(target.membership_expires_at, EXCLUDED.membership_expires_at)`); if (has('credits_balance')) assignments.push(`credits_balance = COALESCE(target.credits_balance, EXCLUDED.credits_balance)`); if (has('daily_quota_limit')) assignments.push(`daily_quota_limit = COALESCE(target.daily_quota_limit, EXCLUDED.daily_quota_limit)`); if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); if (has('preferred_theme')) assignments.push(`preferred_theme = CASE WHEN EXCLUDED.preferred_theme IN ('dark', 'light') THEN EXCLUDED.preferred_theme ELSE target.preferred_theme END`); if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); return assignments; } if (table === 'works') { if (has('user_id')) { assignments.push(`user_id = CASE WHEN (target.user_id IS NULL OR target.user_id = '${SYSTEM_USER_ID}'::uuid) AND EXCLUDED.user_id IS NOT NULL AND EXCLUDED.user_id <> '${SYSTEM_USER_ID}'::uuid THEN EXCLUDED.user_id ELSE target.user_id END`); } if (has('params')) assignments.push(`params = CASE WHEN target.params IS NULL OR target.params = '{}'::jsonb THEN EXCLUDED.params ELSE target.params END`); if (has('thumbnail_url')) assignments.push(`thumbnail_url = COALESCE(NULLIF(target.thumbnail_url, ''), EXCLUDED.thumbnail_url)`); if (has('width')) assignments.push(`width = COALESCE(target.width, EXCLUDED.width)`); if (has('height')) assignments.push(`height = COALESCE(target.height, EXCLUDED.height)`); if (has('duration')) assignments.push(`duration = COALESCE(target.duration, EXCLUDED.duration)`); if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); return assignments; } if (table === 'user_api_keys') { if (has('user_id')) assignments.push(`user_id = COALESCE(target.user_id, EXCLUDED.user_id)`); if (has('provider')) assignments.push(`provider = COALESCE(NULLIF(target.provider, ''), EXCLUDED.provider)`); if (has('supplier_name')) assignments.push(`supplier_name = COALESCE(NULLIF(target.supplier_name, ''), EXCLUDED.supplier_name)`); if (has('api_url')) assignments.push(`api_url = COALESCE(NULLIF(target.api_url, ''), EXCLUDED.api_url)`); if (has('model_name')) assignments.push(`model_name = COALESCE(NULLIF(target.model_name, ''), EXCLUDED.model_name)`); if (has('note')) assignments.push(`note = COALESCE(NULLIF(target.note, ''), EXCLUDED.note)`); if (has('api_key_encrypted')) assignments.push(`api_key_encrypted = COALESCE(NULLIF(target.api_key_encrypted, ''), EXCLUDED.api_key_encrypted)`); if (has('api_key_preview')) assignments.push(`api_key_preview = COALESCE(NULLIF(target.api_key_preview, ''), EXCLUDED.api_key_preview)`); if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); return assignments; } if (table === 'system_api_configs') { if (has('provider')) assignments.push(`provider = COALESCE(NULLIF(target.provider, ''), EXCLUDED.provider)`); if (has('name')) assignments.push(`name = COALESCE(NULLIF(target.name, ''), EXCLUDED.name)`); if (has('api_url')) assignments.push(`api_url = COALESCE(NULLIF(target.api_url, ''), EXCLUDED.api_url)`); if (has('model_name')) assignments.push(`model_name = COALESCE(NULLIF(target.model_name, ''), EXCLUDED.model_name)`); if (has('note')) assignments.push(`note = COALESCE(NULLIF(target.note, ''), EXCLUDED.note)`); if (has('api_key_encrypted')) assignments.push(`api_key_encrypted = COALESCE(NULLIF(target.api_key_encrypted, ''), EXCLUDED.api_key_encrypted)`); if (has('api_key_preview')) assignments.push(`api_key_preview = COALESCE(NULLIF(target.api_key_preview, ''), EXCLUDED.api_key_preview)`); if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); if (has('credits_per_use')) assignments.push(`credits_per_use = COALESCE(target.credits_per_use, EXCLUDED.credits_per_use)`); if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); if (has('sort_order')) assignments.push(`sort_order = COALESCE(target.sort_order, EXCLUDED.sort_order)`); if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); return assignments; } if (table === 'payment_methods') { if (has('type')) assignments.push(`type = COALESCE(NULLIF(target.type, ''), EXCLUDED.type)`); if (has('name')) assignments.push(`name = COALESCE(NULLIF(target.name, ''), EXCLUDED.name)`); if (has('is_active')) assignments.push(`is_active = COALESCE(target.is_active, EXCLUDED.is_active)`); if (has('public_config')) assignments.push(`public_config = COALESCE(target.public_config, EXCLUDED.public_config)`); if (has('secret_config_encrypted')) assignments.push(`secret_config_encrypted = COALESCE(target.secret_config_encrypted, EXCLUDED.secret_config_encrypted)`); if (has('secret_config_preview')) assignments.push(`secret_config_preview = COALESCE(target.secret_config_preview, EXCLUDED.secret_config_preview)`); if (has('updated_at')) assignments.push(`updated_at = GREATEST(COALESCE(target.updated_at, EXCLUDED.updated_at), COALESCE(EXCLUDED.updated_at, target.updated_at))`); return assignments; } return assignments; } async function getExistingColumns( client: Awaited>, table: string, context: ImportContext, ): Promise> { const cached = context.columnCache.get(table); if (cached) return cached; const [schemaName, tableName] = table.includes('.') ? table.split('.', 2) : ['public', table]; const result = await client.query( 'SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2', [schemaName, tableName], ); const columns = new Set((result.rows || []).map((row: Record) => String(row.column_name))); context.columnCache.set(table, columns); return columns; } async function getDefaultableColumns( client: Awaited>, table: string, context: ImportContext, ): Promise> { const cached = context.defaultableColumnCache.get(table); if (cached) return cached; const [schemaName, tableName] = table.includes('.') ? table.split('.', 2) : ['public', table]; const result = await client.query( `SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND is_nullable = 'NO' AND column_default IS NOT NULL`, [schemaName, tableName], ); const columns = new Set((result.rows || []).map((row: Record) => String(row.column_name))); context.defaultableColumnCache.set(table, columns); return columns; } function seedUuidMap(map: Map, value: unknown): void { if (typeof value === 'string' && value && !isUuid(value) && !map.has(value)) { map.set(value, crypto.randomUUID()); } } function isUuid(value: unknown): value is string { return typeof value === 'string' && UUID_REGEX.test(value); } function isDataUrl(value: unknown): boolean { return typeof value === 'string' && /^data:[^,]+,/i.test(value); } function getWorkMediaFolder(type: unknown, kind: string): string { const text = typeof type === 'string' ? type.toLowerCase() : ''; const media = text.includes('video') ? 'videos' : 'images'; return `imported/works/${kind}/${media}`; } function extensionFromMime(mime: string): string { const normalized = mime.toLowerCase(); if (normalized.includes('png')) return 'png'; if (normalized.includes('jpeg') || normalized.includes('jpg')) return 'jpg'; if (normalized.includes('webp')) return 'webp'; if (normalized.includes('gif')) return 'gif'; if (normalized.includes('mp4')) return 'mp4'; if (normalized.includes('webm')) return 'webm'; return 'bin'; } async function persistImportMedia(value: string, folder: string): Promise { if (!isDataUrl(value)) return value; const match = value.match(/^data:([^;,]+)?(;base64)?,([\s\S]*)$/i); if (!match) return value; const mime = match[1] || 'application/octet-stream'; const isBase64 = Boolean(match[2]); const payload = match[3] || ''; const buffer = isBase64 ? Buffer.from(payload, 'base64') : Buffer.from(decodeURIComponent(payload)); const ext = extensionFromMime(mime); const key = `${folder}/${Date.now()}-${crypto.randomUUID()}.${ext}`; const savedKey = await localStorage.uploadFile({ fileContent: buffer, fileName: key, contentType: mime }); return localStorage.generatePresignedUrl({ key: savedKey, expireTime: 2592000 }); } async function sanitizeImportMedia(value: unknown, folder: string): Promise { if (typeof value === 'string') { return persistImportMedia(value, folder); } if (Array.isArray(value)) { return Promise.all(value.map(item => sanitizeImportMedia(item, folder))); } if (value && typeof value === 'object') { const output: Record = {}; for (const [key, nested] of Object.entries(value as Record)) { output[key] = await sanitizeImportMedia(nested, folder); } return output; } return value; }