/** * Seed Khanegi defaultSnapshot from current appUiConfig (or color-built UI). * Optionally restore current appearance to that snapshot (--restore). * * Usage: * npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts * npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts --restore * npx ts-node --transpile-only prisma/seed-khanegi-default-snapshots.ts --restore --domain=example.ir */ import * as fs from 'fs'; import * as path from 'path'; import { PrismaClient, ProductType } from '@prisma/client'; import { buildKhanegiUiFromColors } from './lib/appearance-builders'; const prisma = new PrismaClient(); type SourceUser = { id: number; domain?: string | null; domains?: string | string[] | null; api_origin?: string | null; color?: string | null; secondary_color?: string | null; background_color?: string | null; app_ui_config?: unknown; deleted_at?: string | null; }; function parseArgs() { const restore = process.argv.includes('--restore'); const domainArg = process.argv.find((a) => a.startsWith('--domain=')); const domain = domainArg ? domainArg.slice('--domain='.length).trim().toLowerCase() : null; return { restore, domain }; } function parseDomains(raw: SourceUser['domains']): string[] { if (!raw) return []; try { const value = typeof raw === 'string' ? JSON.parse(raw) : raw; if (!Array.isArray(value)) return []; return value .map((item) => String(item).trim().toLowerCase()) .filter(Boolean) .map((item) => item.replace(/^https?:\/\//, '').split('/')[0]); } catch { return []; } } function primaryDomain(row: SourceUser): string | null { const fromField = row.domain?.trim().toLowerCase(); if (fromField) return fromField.replace(/^https?:\/\//, '').split('/')[0]; const fromOrigin = row.api_origin?.trim().toLowerCase(); if (fromOrigin) return fromOrigin.replace(/^https?:\/\//, '').split('/')[0]; return parseDomains(row.domains)[0] || null; } function cloneSnap(ui: any) { return { app: JSON.parse(JSON.stringify(ui.app)), pages: JSON.parse(JSON.stringify(ui.pages || {})), }; } function stripMeta(ui: any) { if (!ui || typeof ui !== 'object') return ui; const { defaultSnapshot: _s, _backfill, ...rest } = ui; return rest; } async function main() { const { restore, domain: onlyDomain } = parseArgs(); const file = path.join(__dirname, 'data', 'users.json'); const byDomain = new Map(); if (fs.existsSync(file)) { const rows = JSON.parse(fs.readFileSync(file, 'utf8')) as SourceUser[]; for (const row of rows) { if (row.deleted_at) continue; const d = primaryDomain(row); if (d && !byDomain.has(d)) byDomain.set(d, row); for (const extra of parseDomains(row.domains)) { if (!byDomain.has(extra)) byDomain.set(extra, row); } } } const tenants = await prisma.tenant.findMany({ where: { product: ProductType.khanegi }, }); const filtered = onlyDomain ? tenants.filter((tenant) => { const keys = [ tenant.domain?.toLowerCase(), ...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) => String(d).toLowerCase(), ), ]; return keys.includes(onlyDomain); }) : tenants; let updated = 0; let restored = 0; let skipped = 0; for (const tenant of filtered) { const domainKeys = [ tenant.domain?.toLowerCase(), ...((Array.isArray(tenant.domains) ? tenant.domains : []) as string[]).map((d) => String(d).toLowerCase(), ), ].filter(Boolean) as string[]; const source = domainKeys.map((d) => byDomain.get(d)).find(Boolean) || null; const existing = tenant.appUiConfig && typeof tenant.appUiConfig === 'object' ? (tenant.appUiConfig as Record) : null; const fromRegistry = source?.app_ui_config && typeof source.app_ui_config === 'object' && ((source.app_ui_config as any).schemaId === 'ui-config' || (source.app_ui_config as any).app) ? (source.app_ui_config as Record) : null; const built = existing && (existing.schemaId === 'ui-config' || existing.app) ? existing : buildKhanegiUiFromColors({ primary: source?.color || tenant.primaryColor, secondary: source?.secondary_color || tenant.secondaryColor, background: source?.background_color || tenant.backgroundColor, existing: source?.app_ui_config, }); // Factory look: never overwrite existing snapshot; else registry JSON, else current UI const keepSnapshot = existing?.defaultSnapshot && typeof existing.defaultSnapshot === 'object' ? existing.defaultSnapshot : fromRegistry ? cloneSnap(fromRegistry) : cloneSnap(built); const nextUi = restore ? { ...stripMeta(built), schemaId: built.schemaId || 'ui-config', version: built.version || '1.0.0', app: JSON.parse(JSON.stringify(keepSnapshot.app)), pages: JSON.parse(JSON.stringify(keepSnapshot.pages || {})), defaultSnapshot: keepSnapshot, } : { ...stripMeta(built), defaultSnapshot: keepSnapshot, }; const unchanged = JSON.stringify(existing) === JSON.stringify(nextUi) && !restore; if (unchanged) { skipped += 1; continue; } const colors = nextUi.app?.theme?.colors || {}; await prisma.tenant.update({ where: { id: tenant.id }, data: { appUiConfig: nextUi as any, ...(restore ? { primaryColor: colors.primary ?? tenant.primaryColor, secondaryColor: colors.secondary ?? tenant.secondaryColor, backgroundColor: colors.background ?? tenant.backgroundColor, } : {}), }, }); updated += 1; if (restore) restored += 1; console.log(`${restore ? 'restored' : 'seeded'}: ${tenant.domain}`); } console.log({ tenants: filtered.length, updated, restored, skipped, restore, domain: onlyDomain, }); } main() .catch((err) => { console.error(err); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });