chore: initialize WanderQ MiniAPP 2.0 repository
This commit is contained in:
218
apps/api/src/home-modules.ts
Normal file
218
apps/api/src/home-modules.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user