import { Types } from "mongoose"; import { cleanMongoData, extractDumpData, getCollectionData } from "./utils/extractDumpData"; import { Logger } from "../../core/logging/logger"; import { CategoryModel } from "../../modules/category/models/category.model"; import { ThemeModel } from "../../modules/category/models/theme.model"; import { ThemeValueModel } from "../../modules/category/models/themeValue.model"; /** * Maps theme string from dump to Persian theme title */ const mapThemeStringToTitle = (themeString: string | null | undefined): string | null => { if (!themeString || themeString === "noColor_noSize") return null; const themeMap: { [key: string]: string } = { Color: "رنگ", Size: "سایز", Meterage: "متراژ", }; return themeMap[themeString] || null; }; export const seedCategories = async (logger: Logger) => { try { logger.info("Categories seeding started"); // Extract data from dump const dumpData = extractDumpData(); const categoriesData = getCollectionData(dumpData, "categories"); if (categoriesData.length === 0) { logger.warn("No category data found in dump, skipping category seeding"); return; } // Get all themes and create a map const themes = await ThemeModel.find(); const themeMap = new Map(); themes.forEach((theme) => { themeMap.set(theme.title, theme._id); }); // Get all theme values and create maps by theme const themeValues = await ThemeValueModel.find(); const themeValueMap = new Map(); themes.forEach((theme) => { const values = themeValues.filter((tv) => tv.theme.toString() === theme._id.toString()).map((tv) => tv._id); themeValueMap.set(theme._id, values); }); // Clean and prepare category data const cleanedCategories = categoriesData.map((cat) => cleanMongoData(cat)); // Filter out deleted categories const activeCategories = cleanedCategories.filter((cat) => !cat.deleted); // Create a map to store created categories by their old _id const categoryIdMap = new Map(); // First pass: Create all categories without parent relationships interface CategoryToCreate { oldId: string; data: { title_fa: string; title_en: string; icon: string; imageUrl: string; description: string; leaf: boolean; theme?: Types.ObjectId; variants: Types.ObjectId[]; deleted: boolean; }; parentOldId?: string; } const categoriesToCreate: CategoryToCreate[] = []; for (const cat of activeCategories) { const oldId = cat._id?.toString() || ""; const parentOldId = cat.parent?.toString(); // Map theme string to theme ObjectId let themeId: Types.ObjectId | undefined; let variants: Types.ObjectId[] = []; const themeTitle = mapThemeStringToTitle(cat.theme); if (themeTitle) { themeId = themeMap.get(themeTitle); if (themeId) { // Get all theme values for this theme variants = themeValueMap.get(themeId) || []; } } // Map variants from dump if they exist (they might be old IDs, so we'll use all theme values) // If the category has specific variants in the dump, we could map them here // For now, we'll use all theme values for the theme const categoryData = { title_fa: cat.title_fa, title_en: cat.title_en, icon: cat.icon || "/images/icons/default.png", imageUrl: cat.imageUrl || "", description: cat.description || "", leaf: cat.leaf !== undefined ? cat.leaf : true, theme: themeId, variants: variants, deleted: false, }; categoriesToCreate.push({ oldId, data: categoryData, parentOldId, }); } // Sort categories: root categories first (no parent), then children const rootCategories = categoriesToCreate.filter((cat) => !cat.parentOldId); const childCategories = categoriesToCreate.filter((cat) => cat.parentOldId); // Create root categories first for (const { oldId, data } of rootCategories) { try { const category = new CategoryModel(data); await category.save(); categoryIdMap.set(oldId, category._id); logger.info(`Created root category: ${data.title_fa}`); } catch (error: unknown) { // Handle duplicate key error (title_en must be unique) if (error && typeof error === "object" && "code" in error && error.code === 11000) { const existing = await CategoryModel.findOne({ title_en: data.title_en }); if (existing) { categoryIdMap.set(oldId, existing._id); logger.warn(`Category with title_en '${data.title_en}' already exists, using existing`); } } else { logger.error(`Error creating category ${data.title_fa}:`, error); } } } // Create child categories with parent relationships // We need to process them in levels (children of roots, then grandchildren, etc.) let remainingCategories = [...childCategories]; const maxIterations = 10; // Prevent infinite loops let iteration = 0; while (remainingCategories.length > 0 && iteration < maxIterations) { iteration++; const categoriesToProcess = remainingCategories.filter((cat) => { // Check if parent exists in our map return cat.parentOldId && categoryIdMap.has(cat.parentOldId); }); if (categoriesToProcess.length === 0) { // No more categories can be processed (orphaned categories) logger.warn(`Stopped processing: ${remainingCategories.length} categories have missing parents`); break; } for (const { oldId, data, parentOldId } of categoriesToProcess) { try { const parentId = parentOldId ? categoryIdMap.get(parentOldId) : undefined; if (!parentId) { logger.warn(`Parent not found for category ${data.title_fa}, skipping`); continue; } // Build hierarchy from parent const parentCategory = await CategoryModel.findById(parentId); const hierarchy = parentCategory?.hierarchy ? [...parentCategory.hierarchy, parentId] : [parentId]; const category = new CategoryModel({ ...data, parent: parentId, hierarchy: hierarchy, }); await category.save(); categoryIdMap.set(oldId, category._id); logger.info(`Created category: ${data.title_fa} (parent: ${parentCategory?.title_fa})`); } catch (error: unknown) { // Handle duplicate key error if (error && typeof error === "object" && "code" in error && error.code === 11000) { const existing = await CategoryModel.findOne({ title_en: data.title_en }); if (existing) { categoryIdMap.set(oldId, existing._id); logger.warn(`Category with title_en '${data.title_en}' already exists, using existing`); } } else { logger.error(`Error creating category ${data.title_fa}:`, error); } } } // Remove processed categories remainingCategories = remainingCategories.filter((cat) => !categoriesToProcess.includes(cat)); } if (remainingCategories.length > 0) { logger.warn(`${remainingCategories.length} categories could not be created due to missing parents`); } logger.info(`Categories seeded successfully! Created ${categoryIdMap.size} categories`); } catch (error) { logger.error("Error seeding categories:", error); throw error; } };