/** * Menu import functions * * Import navigation menus from WordPress WXR exports or plugin API. */ import type { Kysely } from "kysely"; import { ulid } from "ulidx"; import type { WxrNavMenu, WxrNavMenuItem } from "../cli/wxr/parser.js"; import type { Database } from "../database/types.js"; import type { MenuItemType } from "../menus/types.js"; /** * Result of menu import operation */ export interface MenuImportResult { /** Number of menus created */ menusCreated: number; /** Number of menu items created */ itemsCreated: number; /** Mapping from WP menu slug to EmDash menu ID */ menuIdMap: Map; /** Errors encountered during import */ errors: Array<{ menu: string; error: string }>; } /** * Plugin API menu format (matches /emdash/v1/menus response) */ export interface PluginMenu { id: number; name: string; // slug label: string; items: PluginMenuItem[]; } export interface PluginMenuItem { id: number; parent_id: number | null; sort_order: number; type: "custom" | "post_type" | "taxonomy"; object: string | null; // 'page', 'post', 'category' object_id: number | null; url: string; title: string; target: string | null; classes: string | null; } /** * Import navigation menus from WXR export * * @param menus - Parsed navigation menus from WXR * @param db - Database connection * @param contentIdMap - Map from WP post ID to EmDash content ID (for resolving references) * @returns Import result with counts and ID mapping */ export async function importMenusFromWxr( menus: WxrNavMenu[], db: Kysely, contentIdMap: Map, ): Promise { const result: MenuImportResult = { menusCreated: 0, itemsCreated: 0, menuIdMap: new Map(), errors: [], }; for (const menu of menus) { try { // Check if menu already exists const existing = await db .selectFrom("_emdash_menus") .select("id") .where("name", "=", menu.name) .executeTakeFirst(); if (existing) { result.menuIdMap.set(menu.name, existing.id); continue; // Skip existing menus } // Create the menu const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: menu.name, label: menu.label, }) .execute(); result.menusCreated++; result.menuIdMap.set(menu.name, menuId); // Import menu items const itemsCreated = await importWxrMenuItems(menu.items, menuId, db, contentIdMap); result.itemsCreated += itemsCreated; } catch (error) { result.errors.push({ menu: menu.name, error: error instanceof Error ? error.message : String(error), }); } } return result; } /** * Import navigation menus from Plugin API * * @param menus - Menus from plugin API * @param db - Database connection * @param contentIdMap - Map from WP post ID to EmDash content ID * @returns Import result with counts and ID mapping */ export async function importMenusFromPlugin( menus: PluginMenu[], db: Kysely, contentIdMap: Map, ): Promise { const result: MenuImportResult = { menusCreated: 0, itemsCreated: 0, menuIdMap: new Map(), errors: [], }; for (const menu of menus) { try { // Check if menu already exists const existing = await db .selectFrom("_emdash_menus") .select("id") .where("name", "=", menu.name) .executeTakeFirst(); if (existing) { result.menuIdMap.set(menu.name, existing.id); continue; } // Create the menu const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: menu.name, label: menu.label, }) .execute(); result.menusCreated++; result.menuIdMap.set(menu.name, menuId); // Import menu items const itemsCreated = await importPluginMenuItems(menu.items, menuId, db, contentIdMap); result.itemsCreated += itemsCreated; } catch (error) { result.errors.push({ menu: menu.name, error: error instanceof Error ? error.message : String(error), }); } } return result; } /** * Import menu items from WXR format */ async function importWxrMenuItems( items: WxrNavMenuItem[], menuId: string, db: Kysely, contentIdMap: Map, ): Promise { // Build a map of WP menu item IDs to EmDash IDs for parent resolution const itemIdMap = new Map(); let count = 0; // Sort items by sort order to maintain hierarchy const sortedItems = items.toSorted((a, b) => a.sortOrder - b.sortOrder); // First pass: create all items with temporary parent IDs for (const item of sortedItems) { const itemId = ulid(); itemIdMap.set(item.id, itemId); const { type, collection, referenceId, customUrl } = mapWxrMenuItem(item, contentIdMap); await db .insertInto("_emdash_menu_items") .values({ id: itemId, menu_id: menuId, parent_id: null, // Will be set in second pass sort_order: item.sortOrder, type, reference_collection: collection, reference_id: referenceId, custom_url: customUrl, label: item.title, title_attr: null, target: item.target || null, css_classes: item.classes || null, }) .execute(); count++; } // Second pass: update parent IDs for (const item of sortedItems) { if (item.parentId) { const itemId = itemIdMap.get(item.id); const parentId = itemIdMap.get(item.parentId); if (itemId && parentId) { await db .updateTable("_emdash_menu_items") .set({ parent_id: parentId }) .where("id", "=", itemId) .execute(); } } } return count; } /** * Import menu items from Plugin API format */ async function importPluginMenuItems( items: PluginMenuItem[], menuId: string, db: Kysely, contentIdMap: Map, ): Promise { const itemIdMap = new Map(); let count = 0; const sortedItems = items.toSorted((a, b) => a.sort_order - b.sort_order); for (const item of sortedItems) { const itemId = ulid(); itemIdMap.set(item.id, itemId); const { type, collection, referenceId, customUrl } = mapPluginMenuItem(item, contentIdMap); await db .insertInto("_emdash_menu_items") .values({ id: itemId, menu_id: menuId, parent_id: null, sort_order: item.sort_order, type, reference_collection: collection, reference_id: referenceId, custom_url: customUrl, label: item.title, title_attr: null, target: item.target || null, css_classes: item.classes || null, }) .execute(); count++; } // Second pass: update parent IDs for (const item of sortedItems) { if (item.parent_id) { const itemId = itemIdMap.get(item.id); const parentId = itemIdMap.get(item.parent_id); if (itemId && parentId) { await db .updateTable("_emdash_menu_items") .set({ parent_id: parentId }) .where("id", "=", itemId) .execute(); } } } return count; } /** * Map WXR menu item to EmDash format */ function mapWxrMenuItem( item: WxrNavMenuItem, contentIdMap: Map, ): { type: MenuItemType; collection: string | null; referenceId: string | null; customUrl: string | null; } { switch (item.type) { case "custom": return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; case "post_type": { // Map WordPress object type to collection const collection = mapObjectToCollection(item.objectType); const referenceId = item.objectId ? contentIdMap.get(item.objectId) || null : null; // If we can't resolve the reference, fall back to custom URL if (!referenceId && item.url) { return { type: "custom", collection: null, referenceId: null, customUrl: item.url, }; } return { type: collection === "pages" ? "page" : "post", collection, referenceId, customUrl: null, }; } case "taxonomy": // For taxonomies, we need taxonomy support in menus // Fall back to custom URL for now return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; default: return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; } } /** * Map Plugin menu item to EmDash format */ function mapPluginMenuItem( item: PluginMenuItem, contentIdMap: Map, ): { type: MenuItemType; collection: string | null; referenceId: string | null; customUrl: string | null; } { switch (item.type) { case "custom": return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; case "post_type": { const collection = mapObjectToCollection(item.object); const referenceId = item.object_id ? contentIdMap.get(item.object_id) || null : null; if (!referenceId && item.url) { return { type: "custom", collection: null, referenceId: null, customUrl: item.url, }; } return { type: collection === "pages" ? "page" : "post", collection, referenceId, customUrl: null, }; } case "taxonomy": return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; default: return { type: "custom", collection: null, referenceId: null, customUrl: item.url || "#", }; } } /** * Map WordPress object type to EmDash collection name */ function mapObjectToCollection(objectType: string | undefined | null): string { if (!objectType) return "posts"; const mapping: Record = { post: "posts", page: "pages", product: "products", portfolio: "portfolio", }; return mapping[objectType] || objectType; }