import { Prisma } from "@prisma/client"; import { prisma } from "./prisma.js"; import { cloneHomeModuleContent, createHomeModuleSnapshot, DEFAULT_HOME_MODULES, HOME_MODULE_TEMPLATE_MAP, type HomeModuleContent, type HomeModuleSnapshot, type HomeModuleTemplateType, } from "./home-module-defaults.js"; type HomeModuleRow = Awaited>[number]; const LEGACY_HOME_MODULE_IDS = ["routes"] as const; export type HomeModuleResponse = { id: string; label: string; sortOrder: number; isActive: boolean; templateType: HomeModuleTemplateType; content: HomeModuleContent; isSystem: boolean; isDeleted: boolean; publishedConfig?: HomeModuleSnapshot | null; }; function asContent(value: Prisma.JsonValue | null | undefined, fallback: HomeModuleContent): HomeModuleContent { if (!value || typeof value !== "object" || Array.isArray(value)) return cloneHomeModuleContent(fallback); const candidate = value as { items?: unknown }; if (!Array.isArray(candidate.items)) return cloneHomeModuleContent(fallback); return candidate as HomeModuleContent; } function asTemplateType(value: string, fallback: HomeModuleTemplateType): HomeModuleTemplateType { return value in HOME_MODULE_TEMPLATE_MAP ? (value as HomeModuleTemplateType) : fallback; } function asSnapshot(value: Prisma.JsonValue | null | undefined): HomeModuleSnapshot | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const candidate = value as Partial; if ( typeof candidate.label !== "string" || typeof candidate.sortOrder !== "number" || typeof candidate.isActive !== "boolean" || typeof candidate.templateType !== "string" || !candidate.content || typeof candidate.content !== "object" ) { return null; } return { label: candidate.label, sortOrder: candidate.sortOrder, isActive: candidate.isActive, templateType: asTemplateType(candidate.templateType, "explore"), content: asContent(candidate.content as Prisma.JsonValue, { items: [] }), isDeleted: candidate.isDeleted === true, }; } function snapshotForRow(row: HomeModuleRow): HomeModuleSnapshot { const templateType = asTemplateType(row.templateType, "explore"); const fallback = HOME_MODULE_TEMPLATE_MAP[templateType].defaultContent; return createHomeModuleSnapshot({ label: row.label, sortOrder: row.sortOrder, isActive: row.isActive, templateType, content: asContent(row.content, fallback), isDeleted: row.isDeleted, }); } function toResponse(row: HomeModuleRow, published: boolean): HomeModuleResponse | null { const draftTemplateType = asTemplateType(row.templateType, "explore"); const draftContent = asContent(row.content, HOME_MODULE_TEMPLATE_MAP[draftTemplateType].defaultContent); const publishedConfig = asSnapshot(row.publishedConfig); if (published) { if (!publishedConfig || publishedConfig.isDeleted || !publishedConfig.isActive) return null; return { id: row.id, label: publishedConfig.label, sortOrder: publishedConfig.sortOrder, isActive: true, templateType: publishedConfig.templateType, content: publishedConfig.content, isSystem: row.isSystem, isDeleted: false, }; } if (row.isDeleted) return null; return { id: row.id, label: row.label, sortOrder: row.sortOrder, isActive: row.isActive, templateType: draftTemplateType, content: draftContent, isSystem: row.isSystem, isDeleted: row.isDeleted, publishedConfig, }; } async function ensureDefaultHomeModules() { const current = await prisma.homeModule.findMany(); const currentById = new Map(current.map((module) => [module.id, module])); for (const legacyId of LEGACY_HOME_MODULE_IDS) { const existing = currentById.get(legacyId); if (!existing) continue; const publishedConfig = asSnapshot(existing.publishedConfig); const needsRetire = !existing.isDeleted || existing.isActive || Boolean(publishedConfig && (!publishedConfig.isActive || !publishedConfig.isDeleted)); if (!needsRetire) continue; await prisma.homeModule.update({ where: { id: legacyId }, data: { isActive: false, isDeleted: true, publishedConfig: publishedConfig ? ({ ...publishedConfig, isActive: false, isDeleted: true } as Prisma.InputJsonValue) : undefined, }, }); } for (const fallback of DEFAULT_HOME_MODULES) { const existing = currentById.get(fallback.id); if (!existing) { const snapshot = createHomeModuleSnapshot(fallback); await prisma.homeModule.create({ data: { id: fallback.id, label: fallback.label, templateType: fallback.templateType, content: fallback.content as Prisma.InputJsonValue, publishedConfig: snapshot as Prisma.InputJsonValue, sortOrder: fallback.sortOrder, isActive: fallback.isActive, isSystem: true, isDeleted: false, }, }); continue; } const templateType = fallback.templateType; const content = asContent(existing.content, fallback.content); const publishedConfig = asSnapshot(existing.publishedConfig) ?? createHomeModuleSnapshot({ label: existing.label, sortOrder: existing.sortOrder, isActive: existing.isActive, templateType, content, isDeleted: false, }); const needsContent = !existing.content; const needsPublished = !existing.publishedConfig; const needsTemplate = existing.templateType !== templateType; const needsSystemFlag = !existing.isSystem; if (needsContent || needsPublished || needsTemplate || needsSystemFlag) { await prisma.homeModule.update({ where: { id: fallback.id }, data: { templateType, content: needsContent ? (content as Prisma.InputJsonValue) : undefined, publishedConfig: needsPublished ? (publishedConfig as Prisma.InputJsonValue) : undefined, isSystem: true, }, }); } } } export async function listHomeModules(activeOnly = false, published = false) { await ensureDefaultHomeModules(); const rows = await prisma.homeModule.findMany({ orderBy: [{ sortOrder: "asc" }, { id: "asc" }] }); return rows .map((row) => toResponse(row, published)) .filter((row): row is HomeModuleResponse => Boolean(row)) .filter((row) => !activeOnly || row.isActive) .sort((a, b) => a.sortOrder - b.sortOrder || a.id.localeCompare(b.id)); } export function templateContent(templateType: HomeModuleTemplateType) { return cloneHomeModuleContent(HOME_MODULE_TEMPLATE_MAP[templateType].defaultContent); } export function moduleSnapshotFromDraft(row: { label: string; sortOrder: number; isActive: boolean; templateType: string; content: Prisma.JsonValue | null; isDeleted: boolean; }) { const templateType = asTemplateType(row.templateType, "explore"); return createHomeModuleSnapshot({ label: row.label, sortOrder: row.sortOrder, isActive: row.isActive, templateType, content: asContent(row.content, HOME_MODULE_TEMPLATE_MAP[templateType].defaultContent), isDeleted: row.isDeleted, }); } export function isHomeModuleTemplate(value: string): value is HomeModuleTemplateType { return value in HOME_MODULE_TEMPLATE_MAP; }