Files
shop-api/src/db/seeders/categorySeeder.ts
T
morteza-mortezai 1345f81b3a db: seeder
2025-11-30 21:45:24 +03:30

212 lines
7.6 KiB
TypeScript

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<string, Types.ObjectId>();
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<Types.ObjectId, Types.ObjectId[]>();
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<string, Types.ObjectId>();
// 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;
}
};