Files
WanderQ-MiniAPP2.0/apps/api/src/home-modules.ts

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;
}