219 lines
7.3 KiB
TypeScript
219 lines
7.3 KiB
TypeScript
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<ReturnType<typeof prisma.homeModule.findMany>>[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<HomeModuleSnapshot>;
|
|
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;
|
|
}
|